cloudinary 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ pkg
2
+ .project
3
+ .rvmrc
4
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in cloudinary.gemspec
4
+ gemspec
@@ -0,0 +1,46 @@
1
+ Cloudinary
2
+ ==========
3
+
4
+ Cloudinary allows web applications to manage web resources in the cloud leveraging cloud-solutions.
5
+ Cloudinary offers a solution to the entire asset management workflow, from upload to transformations, optimizations, storage and delivery.
6
+
7
+ ## Setup ######################################################################
8
+
9
+ To install the Cloudinary Ruby GEM, run:
10
+
11
+ $ gem install cloudinary
12
+
13
+ I you use Rails 3.x or higher, edit your Gemfile, add the following line and run 'bundle'
14
+
15
+ $ gem 'cloudinary'
16
+
17
+ Or in Rails 2.x, edit your environment.rb and add:
18
+
19
+ $ config.gem 'cloudinary'
20
+
21
+ If you would like to use our optional integration module of image uploads with ActiveRecord using CarrierWave, install CarrierWave to:
22
+
23
+ $ gem install carrierwave
24
+
25
+ Rails 3.x Gemfile:
26
+
27
+ $ gem 'carrierwave'
28
+
29
+ Rails 2.x environment.rb
30
+
31
+ $ config.gem 'carrierwave', :version => '~> 0.4.1'
32
+
33
+
34
+ ## Usage ######################################################################
35
+
36
+ Refer to Cloudinary Documentation at:
37
+ http://cloudinary.com/documentation
38
+
39
+
40
+ For documetation about Ruby on Rails integration see:
41
+ http://cloudinary.com/documentation/rails_integration
42
+
43
+
44
+ ## License #######################################################################
45
+
46
+ Released under the MIT license.
@@ -0,0 +1,3 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "cloudinary/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "cloudinary"
7
+ s.version = Cloudinary::VERSION
8
+ s.authors = ["Nadav Soferman","Itai Lahan","Tal Lev-Ami"]
9
+ s.email = ["nadav.soferman@cloudinary.com","itai.lahan@cloudinary.com","tal.levami@cloudinary.com"]
10
+ s.homepage = "http://cloudinary.com"
11
+ s.summary = %q{Client library for easily using the Cloudinary service}
12
+ s.description = %q{Client library for easily using the Cloudinary service}
13
+
14
+ s.rubyforge_project = "cloudinary"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency "rest-client"
22
+ end
@@ -0,0 +1,85 @@
1
+ # Copyright Cloudinary
2
+ require "ostruct"
3
+ require "cloudinary/version"
4
+ require "cloudinary/utils"
5
+ require "cloudinary/uploader"
6
+ require "cloudinary/downloader"
7
+ require "cloudinary/migrator"
8
+ require "cloudinary/blob"
9
+ require 'active_support'
10
+ if defined?(::CarrierWave)
11
+ require "cloudinary/carrier_wave"
12
+ end
13
+
14
+ if defined?(::ActionView::Base)
15
+ require "cloudinary/helper"
16
+ end
17
+
18
+ if !nil.respond_to?(:blank?)
19
+ class Object
20
+ def blank?
21
+ respond_to?(:empty?) ? empty? : !self
22
+ end
23
+ end
24
+
25
+ class NilClass #:nodoc:
26
+ def blank?
27
+ true
28
+ end
29
+ end
30
+
31
+ class FalseClass #:nodoc:
32
+ def blank?
33
+ true
34
+ end
35
+ end
36
+
37
+ class TrueClass #:nodoc:
38
+ def blank?
39
+ false
40
+ end
41
+ end
42
+
43
+ class Array #:nodoc:
44
+ alias_method :blank?, :empty?
45
+ end
46
+
47
+ class Hash #:nodoc:
48
+ alias_method :blank?, :empty?
49
+ end
50
+
51
+ class String #:nodoc:
52
+ def blank?
53
+ self !~ /\S/
54
+ end
55
+ end
56
+
57
+ class Numeric #:nodoc:
58
+ def blank?
59
+ false
60
+ end
61
+ end
62
+ end
63
+
64
+ module Cloudinary
65
+ @@config = nil
66
+
67
+ def self.config(new_config=nil)
68
+ @@config = new_config if new_config
69
+ if block_given?
70
+ @@config = OpenStruct.new
71
+ yield(@@config)
72
+ end
73
+ # Heroku support
74
+ if @@config.nil? && ENV["CLOUDINARY_CLOUD_NAME"]
75
+ @@config = OpenStruct.new(
76
+ "cloud_name" => ENV["CLOUDINARY_CLOUD_NAME"],
77
+ "api_key" => ENV["CLOUDINARY_API_KEY"],
78
+ "api_secret" => ENV["CLOUDINARY_API_SECRET"],
79
+ "secure_distribution" => ENV["CLOUDINARY_SECURE_DISTRIBUTION"],
80
+ "private_cdn" => ENV["CLOUDINARY_PRIVATE_CDN"].to_s == 'true'
81
+ )
82
+ end
83
+ @@config ||= OpenStruct.new((YAML.load_file(Rails.root.join("config").join("cloudinary.yml"))[Rails.env] rescue {}))
84
+ end
85
+ end
@@ -0,0 +1,11 @@
1
+ # Copyright Cloudinary
2
+ class Cloudinary::Blob < StringIO
3
+ attr_reader :original_filename, :content_type
4
+ alias_method :path, :original_filename
5
+
6
+ def initialize(data, options={})
7
+ super(data)
8
+ @original_filename = options[:original_filename] || "cloudinaryfile"
9
+ @content_type = options[:content_type] || "application/octet-stream"
10
+ end
11
+ end
@@ -0,0 +1,228 @@
1
+ # Copyright Cloudinary
2
+ require 'pp'
3
+ module Cloudinary::CarrierWave
4
+ class UploadError < StandardError
5
+ attr_reader :http_code
6
+ def initialize(message, http_code)
7
+ super(message)
8
+ @http_code = http_code
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ def eager
14
+ process :eager => true
15
+ end
16
+
17
+ def convert(format)
18
+ process :convert => format
19
+ end
20
+
21
+ def resize_to_limit(width, height)
22
+ process :resize_to_limit => [width, height]
23
+ end
24
+
25
+ def resize_to_fit(width, height)
26
+ process :resize_to_fit => [width, height]
27
+ end
28
+
29
+ def resize_to_fill(width, height, gravity="Center")
30
+ process :resize_to_fill => [width, height, gravity]
31
+ end
32
+
33
+ def resize_and_pad(width, height, background=:transparent, gravity="Center")
34
+ process :resize_and_pad => [width, height, background, gravity]
35
+ end
36
+
37
+ def scale(width, height)
38
+ process :scale => [width, height]
39
+ end
40
+
41
+ def crop(width, height, gravity="Center")
42
+ process :crop => [width, height, gravity]
43
+ end
44
+
45
+ def cloudinary_transformation(options)
46
+ process :cloudinary_transformation => options
47
+ end
48
+
49
+ def tags(*tags)
50
+ process :tags=>tags
51
+ end
52
+ end
53
+
54
+ def self.included(base)
55
+ base.storage Cloudinary::CarrierWave::Storage
56
+ base.extend ClassMethods
57
+ base.send(:attr_accessor, :metadata)
58
+ end
59
+
60
+ def set_or_yell(hash, attr, value)
61
+ raise "conflicting transformation on #{attr} #{value}!=#{hash[attr]}" if hash[attr]
62
+ hash[attr] = value
63
+ end
64
+
65
+ def transformation
66
+ return @transformation if @transformation
67
+ transformation = {}
68
+ self.class.processors.each do
69
+ |name, args|
70
+ case name
71
+ when :convert # Do nothing. This is handled by format
72
+ when :resize_to_limit
73
+ set_or_yell(transformation, :width, args[0])
74
+ set_or_yell(transformation, :height, args[1])
75
+ set_or_yell(transformation, :crop, :limit)
76
+ when :resize_to_fit
77
+ set_or_yell(transformation, :width, args[0])
78
+ set_or_yell(transformation, :height, args[1])
79
+ set_or_yell(transformation, :crop, :fit)
80
+ when :resize_to_fill
81
+ set_or_yell(transformation, :width, args[0])
82
+ set_or_yell(transformation, :height, args[1])
83
+ set_or_yell(transformation, :gravity, args[2].to_s.downcase)
84
+ set_or_yell(transformation, :crop, :fill)
85
+ when :resize_to_pad
86
+ set_or_yell(transformation, :width, args[0])
87
+ set_or_yell(transformation, :height, args[1])
88
+ set_or_yell(transformation, :gravity, args[3].to_s.downcase)
89
+ set_or_yell(transformation, :crop, :pad)
90
+ when :scale
91
+ set_or_yell(transformation, :width, args[0])
92
+ set_or_yell(transformation, :height, args[1])
93
+ set_or_yell(transformation, :crop, :scale)
94
+ when :crop
95
+ set_or_yell(transformation, :width, args[0])
96
+ set_or_yell(transformation, :height, args[1])
97
+ set_or_yell(transformation, :gravity, args[2].to_s.downcase)
98
+ set_or_yell(transformation, :crop, :crop)
99
+ when :cloudinary_transformation
100
+ args.each do
101
+ |attr, value|
102
+ set_or_yell(transformation, attr, value)
103
+ end
104
+ end
105
+ end
106
+ @transformation = transformation
107
+ @transformation
108
+ end
109
+
110
+ def eager
111
+ @eager ||= self.class.processors.any?{|processor| processor[0] == :eager}
112
+ end
113
+
114
+ def tags
115
+ @tags ||= self.class.processors.select{|processor| processor[0] == :tags}.map(&:last).first
116
+ end
117
+
118
+ def format
119
+ format_processor = self.class.processors.find{|processor| processor[0] == :convert}
120
+ if format_processor
121
+ if format_processor[1].is_a?(Array)
122
+ return format_processor[1][0]
123
+ end
124
+ return format_processor[1]
125
+ end
126
+ the_filename = original_filename || stored_filename
127
+ return the_filename.split(".").last if the_filename.include?(".")
128
+ "png" # TODO Default format?
129
+ end
130
+
131
+ def url(*args)
132
+ if(args.first)
133
+ super
134
+ else
135
+ options = self.class.version_names.blank? ? {} : self.transformation
136
+ Cloudinary::Utils.cloudinary_url(self.my_filename, options.clone)
137
+ end
138
+ end
139
+
140
+ def process!(new_file=nil)
141
+ # Do nothing
142
+ end
143
+
144
+ def stored_filename
145
+ @stored_filename ||= model.read_uploader(mounted_as)
146
+ end
147
+
148
+ def my_filename
149
+ @my_filename ||= stored_filename || ("#{self.public_id}.#{self.format}")
150
+ end
151
+
152
+ def public_id
153
+ return @public_id if @public_id
154
+ if stored_filename
155
+ last_dot = stored_filename.rindex(".")
156
+ @public_id = last_dot ? stored_filename[0, last_dot] : stored_filename
157
+ end
158
+ @public_id ||= Cloudinary::Utils.random_public_id
159
+ end
160
+
161
+ def download!(uri)
162
+ uri = process_uri(uri)
163
+ self.original_filename = @cache_id = @filename = File.basename(uri.path).gsub(/[^a-zA-Z0-9\.\-\+_]/, '')
164
+ @file = RemoteFile.new(uri, @filename)
165
+ end
166
+
167
+ def blank?
168
+ self.filename.blank? && self.stored_filename.blank?
169
+ end
170
+
171
+ class RemoteFile
172
+ attr_reader :uri, :original_filename
173
+ def initialize(uri, filename)
174
+ @uri = uri
175
+ @original_filename = filename
176
+ end
177
+
178
+ def delete
179
+ # Do nothing. This is a virtual file.
180
+ end
181
+ end
182
+
183
+ class Storage < ::CarrierWave::Storage::Abstract
184
+ def store!(file)
185
+ # Moved to identifier...
186
+ if uploader.class.version_names.blank?
187
+ # This is the toplevel, need to upload the actual file.
188
+ params = uploader.transformation.dup
189
+ params[:return_error] = true
190
+ params[:format] = uploader.format
191
+ params[:public_id] = uploader.public_id.split("/").last
192
+ params[:tags] = uploader.tags if uploader.tags
193
+ eager_versions = uploader.versions.values.select(&:eager)
194
+ params[:eager] = eager_versions.map{|version| [version.transformation, version.format]} if eager_versions.length > 0
195
+
196
+ data = nil
197
+ if (file.is_a?(RemoteFile))
198
+ data = file.uri.to_s
199
+ else
200
+ data = file.file
201
+ data.rewind if !file.is_path? && data.respond_to?(:rewind)
202
+ end
203
+ uploader.metadata = Cloudinary::Uploader.upload(data, params)
204
+ if uploader.metadata["error"]
205
+ raise UploadError.new(uploader.metadata["error"]["message"], uploader.metadata["error"]["http_code"])
206
+ end
207
+
208
+ if uploader.metadata["version"]
209
+ raise "Only ActiveRecord supported at the moment!" if !(uploader.model.class.respond_to?(:update_all) && uploader.model.class.respond_to?(:primary_key))
210
+ primary_key = uploader.model.class.primary_key.to_sym
211
+ uploader.model.class.update_all(["#{uploader.mounted_as}=?", "v#{uploader.metadata["version"]}/#{identifier.split("/").last}"], {primary_key=>uploader.model.send(primary_key)})
212
+ end
213
+ # Will throw an exception on error
214
+ else
215
+ raise "nested versions are not allowed." if (uploader.class.version_names.length > 1)
216
+ # Do nothing
217
+ end
218
+ end
219
+
220
+ def retrieve!(identifier)
221
+ # Do nothing
222
+ end
223
+
224
+ def identifier
225
+ (uploader.filename || uploader.stored_filename) ? uploader.my_filename : nil
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,21 @@
1
+ # Copyright Cloudinary
2
+ class Cloudinary::Downloader
3
+
4
+ def self.download(source, options={})
5
+ options = options.clone
6
+ if !source.match(/^https?:\/\//i)
7
+ source = Cloudinary::Utils.cloudinary_url(source, options)
8
+ end
9
+
10
+ url = URI.parse(source)
11
+ http = Net::HTTP.new(url.host, url.port)
12
+ req = Net::HTTP::Get.new(url.path)
13
+ if url.port == 443
14
+ http.use_ssl=true
15
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
16
+ end
17
+ res = http.start{|agent| agent.request(req)}
18
+ return res.body
19
+ end
20
+
21
+ end
@@ -0,0 +1,53 @@
1
+ # Copyright Cloudinary
2
+
3
+ module CloudinaryHelper
4
+ # Examples
5
+ # cl_image_tag "israel.png", :width=>100, :height=>100, :alt=>"hello" # W/H are not sent to cloudinary
6
+ # cl_image_tag "israel.png", :width=>100, :height=>100, :alt=>"hello", :crop=>:fit # W/H are sent to cloudinary
7
+ def cl_image_tag(source, options = {})
8
+ options = options.clone
9
+ source = cloudinary_url(source, options)
10
+ options[:width] = options.delete(:html_width) if options.include?(:html_width)
11
+ options[:height] = options.delete(:html_height) if options.include?(:html_height)
12
+
13
+ image_tag(source, options)
14
+ end
15
+
16
+ def facebook_profile_image_tag(profile, options = {})
17
+ cl_image_tag(profile, {:type=>:facebook}.merge(options))
18
+ end
19
+
20
+ def twitter_profile_image_tag(profile, options = {})
21
+ cl_image_tag(profile, {:type=>:twitter}.merge(options))
22
+ end
23
+
24
+ def twitter_name_profile_image_tag(profile, options = {})
25
+ cl_image_tag(profile, {:type=>:twitter_name}.merge(options))
26
+ end
27
+
28
+ def cl_sprite_url(source, options = {})
29
+ options = options.clone
30
+
31
+ version_store = options.delete(:version_store)
32
+ if options[:version].blank? && (version_store == :file) && defined?(Rails) && defined?(Rails.root)
33
+ file_name = "#{Rails.root}/tmp/cloudinary/cloudinary_sprite_#{source.sub(/\..*/, '')}.version"
34
+ if File.exists?(file_name)
35
+ options[:version] = File.read(file_name).chomp
36
+ end
37
+ end
38
+
39
+ options[:format] = "css" unless source.ends_with?(".css")
40
+ cloudinary_url(source, options.merge(:type=>:sprite))
41
+ end
42
+
43
+ def cl_sprite_tag(source, options = {})
44
+ stylesheet_link_tag(cl_sprite_url(source, options))
45
+ end
46
+
47
+ def cloudinary_url(source, options = {})
48
+ options[:secure] = request.ssl? if !options.include?(:secure) && defined?(request) && request && request.respond_to?(:ssl?)
49
+ Cloudinary::Utils.cloudinary_url(source, options)
50
+ end
51
+ end
52
+
53
+ ActionView::Base.send :include, CloudinaryHelper
@@ -0,0 +1,7 @@
1
+ # Copyright Cloudinary
2
+
3
+ module CloudinaryHelper
4
+
5
+ end
6
+
7
+ ActionView::Base.send :include, CloudinaryHelper
@@ -0,0 +1,361 @@
1
+ # Copyright Cloudinary
2
+
3
+ class Cloudinary::Migrator
4
+ attr_reader :retrieve, :complete
5
+ attr_accessor :terminate, :in_process
6
+ attr_reader :db
7
+ attr_reader :work, :results, :mutex
8
+ attr_reader :extra_options
9
+
10
+
11
+ @@init = false
12
+ def self.init
13
+ return if @@init
14
+ @@init = true
15
+
16
+ begin
17
+ require 'sqlite3'
18
+ rescue LoadError
19
+ raise "Please add sqlite3 to your Gemfile"
20
+ end
21
+ require 'tempfile'
22
+ end
23
+
24
+ def json_decode(str)
25
+ Cloudinary::Utils.json_decode(str)
26
+ end
27
+
28
+ def initialize(options={})
29
+ self.class.init
30
+
31
+ options[:db_file] = "tmp/migration#{$$}.db" if options[:private_database] && !options[:db_file]
32
+ @dbfile = options[:db_file] || "tmp/migration.db"
33
+ FileUtils.mkdir_p(File.dirname(@dbfile))
34
+ @db = SQLite3::Database.new @dbfile, :results_as_hash=>true
35
+ @retrieve = options[:retrieve]
36
+ @complete = options[:complete]
37
+ @debug = options[:debug] || false
38
+ @ignore_duplicates = options[:ignore_duplicates]
39
+ @threads = [options[:threads] || 10, 100].min
40
+ @extra_options = {:api_key=>options[:api_key], :api_secret=>options[:api_secret]}
41
+ @delete_after_done = options[:delete_after_done] || options[:private_database]
42
+ @max_processing = @threads * 10
43
+ @in_process = 0
44
+ @work = Queue.new
45
+ @results = Queue.new
46
+ @mutex = Mutex.new
47
+ @db.execute "
48
+ create table if not exists queue (
49
+ id integer primary key,
50
+ internal_id integer,
51
+ public_id text,
52
+ url text,
53
+ metadata text,
54
+ result string,
55
+ status text,
56
+ updated_at integer
57
+ )
58
+ "
59
+ @db.execute "
60
+ create index if not exists status_idx on queue (
61
+ status
62
+ )
63
+ "
64
+ @db.execute "
65
+ create unique index if not exists internal_id_idx on queue (
66
+ internal_id
67
+ )
68
+ "
69
+ @db.execute "
70
+ create unique index if not exists public_id_idx on queue (
71
+ public_id
72
+ )
73
+ "
74
+ if options[:reset_queue]
75
+ @db.execute("delete from queue")
76
+ end
77
+ end
78
+
79
+ def register_retrieve(&block)
80
+ @retrieve = block
81
+ end
82
+
83
+ def register_complete(&block)
84
+ @complete = block
85
+ end
86
+
87
+ def process(options={})
88
+ raise "url not given and no retieve callback given" if options[:url].nil? && self.retrieve.nil?
89
+ raise "id not given and retieve or complete callback given" if options[:id].nil? && (!self.retrieve.nil? || !self.complete.nil?)
90
+
91
+ debug("Process: #{options.inspect}")
92
+ start
93
+ process_results
94
+ wait_for_queue
95
+ options = options.dup
96
+ id = options.delete(:id)
97
+ url = options.delete(:url)
98
+ public_id = options.delete(:public_id)
99
+ row = {
100
+ "internal_id"=>id,
101
+ "url"=>url,
102
+ "public_id"=>public_id,
103
+ "metadata"=>options.to_json,
104
+ "status"=>"processing"
105
+ }
106
+ begin
107
+ insert_row(row)
108
+ add_to_work_queue(row)
109
+ rescue SQLite3::ConstraintException
110
+ raise if !@ignore_duplicates
111
+ end
112
+ end
113
+
114
+ def done
115
+ start
116
+ process_all_pending
117
+ @terminate = true
118
+ 1.upto(@threads){self.work << nil} # enough objects to release all waiting threads
119
+ @started = false
120
+ @db.close
121
+ File.delete(@dbfile) if @delete_after_done
122
+ end
123
+
124
+ def max_given_id
125
+ db.get_first_value("select max(internal_id) from queue").to_i
126
+ end
127
+
128
+ def close_if_needed(file)
129
+ if file.nil?
130
+ # Do nothing.
131
+ elsif file.respond_to?(:close!)
132
+ file.close!
133
+ elsif file.respond_to?(:close)
134
+ file.close
135
+ end
136
+ rescue
137
+ # Ignore errors in closing files
138
+ end
139
+
140
+ def temporary_file(data, filename)
141
+ file = RUBY_VERSION == "1.8.7" ? Tempfile.new('cloudinary') : Tempfile.new('cloudinary', :encoding => 'ascii-8bit')
142
+ file.unlink
143
+ file.write(data)
144
+ file.rewind
145
+ # Tempfile return path == nil after unlink, which break rest-client
146
+ class << file
147
+ attr_accessor :original_filename
148
+ def content_type
149
+ "application/octet-stream"
150
+ end
151
+ end
152
+ file.original_filename = filename
153
+ file
154
+ end
155
+
156
+ private
157
+
158
+ def update_all(values)
159
+ @db.execute("update queue set #{values.keys.map{|key| "#{key}=?"}.join(",")}", *values.values)
160
+ end
161
+
162
+ def update_row(row, values)
163
+ values.merge!("updated_at"=>Time.now.to_i)
164
+ query = ["update queue set #{values.keys.map{|key| "#{key}=?"}.join(",")} where id=?"] + values.values + [row["id"]]
165
+ result = @db.execute(*query)
166
+ values.each{|key, value| row[key.to_s] = value}
167
+ row
168
+ end
169
+
170
+ def insert_row(values)
171
+ values.merge!("updated_at"=>Time.now.to_i)
172
+ @db.execute("insert into queue (#{values.keys.join(",")}) values (#{values.keys.map{"?"}.join(",")})", *values.values)
173
+ values["id"] = @db.last_insert_row_id
174
+ end
175
+
176
+ def refill_queue(last_id)
177
+ @db.execute("select * from queue where status in ('error', 'processing') and id > ? limit ?", last_id, 10000) do
178
+ |row|
179
+ last_id = row["id"] if row["id"] > last_id
180
+ wait_for_queue
181
+ add_to_work_queue(row)
182
+ end
183
+ last_id
184
+ end
185
+
186
+ def process_results
187
+ while self.results.length > 0
188
+ row = self.results.pop
189
+ result = json_decode(row["result"])
190
+ debug("Done ID=#{row['internal_id']}, result=#{result.inspect}")
191
+ complete.call(row["internal_id"], result) if complete
192
+ if result["error"]
193
+ status = case result["error"]["http_code"]
194
+ when 400, 404 then "fatal" # Problematic request. Not a server problem.
195
+ else "error"
196
+ end
197
+ else
198
+ status = "completed"
199
+ end
200
+ updates = {:status=>status, :result=>row["result"]}
201
+ updates["public_id"] = result["public_id"] if result["public_id"] && !row["public_id"]
202
+ begin
203
+ update_row(row, updates)
204
+ rescue SQLite3::ConstraintException
205
+ updates = {:status=>"error", :result=>{:error=>{:message=>"public_id already exists"}}.to_json}
206
+ update_row(row, updates)
207
+ end
208
+ end
209
+ end
210
+
211
+ def try_try_again(tries=5)
212
+ retry_count = 0
213
+ begin
214
+ return yield
215
+ rescue
216
+ retry_count++
217
+ raise if retry_count > tries
218
+ sleep rand * 3
219
+ retry
220
+ end
221
+ end
222
+
223
+ def start
224
+ return if @started
225
+ @started = true
226
+ @terminate = false
227
+
228
+ self.work.clear
229
+
230
+ main = self
231
+ Thread.abort_on_exception = true
232
+ 1.upto(@threads) do
233
+ |i|
234
+ Thread.start do
235
+ while !main.terminate
236
+ file = nil
237
+ row = main.work.pop
238
+ next if row.nil?
239
+ begin
240
+ debug "Thread #{i} - processing row #{row.inspect}. #{main.work.length} work waiting. #{main.results.length} results waiting."
241
+ url = row["url"]
242
+ cw = false
243
+ result = nil
244
+ if url.nil? && !self.retrieve.nil?
245
+ data = self.retrieve.call(row["internal_id"])
246
+ if defined?(ActiveRecord::Base) && data.is_a?(ActiveRecord::Base)
247
+ cw = true
248
+ data.save!
249
+ elsif defined?(Cloudinary::CarrierWave) && data.is_a?(Cloudinary::CarrierWave)
250
+ cw = true
251
+ begin
252
+ data.model.save!
253
+ rescue Cloudinary::CarrierWave::UploadError
254
+ # upload errors will be handled by the result values.
255
+ end
256
+ result = data.metadata
257
+ elsif data.respond_to?(:read) && data.respond_to?(:path)
258
+ # This is an IO style object, pass as is.
259
+ file = data
260
+ elsif !data.nil?
261
+ file = main.temporary_file(data, row["public_id"] || "cloudinaryfile")
262
+ end
263
+ end
264
+
265
+ if url || file
266
+ options = main.extra_options.merge(:public_id=>row["public_id"])
267
+ json_decode(row["metadata"]).each do
268
+ |key, value|
269
+ options[key.to_sym] = value
270
+ end
271
+
272
+ result = Cloudinary::Uploader.upload(url || file, options.merge(:return_error=>true)) || ({:error=>{:message=>"Received nil from uploader!"}})
273
+ elsif cw
274
+ result ||= {"status" => "saved"}
275
+ else
276
+ result = {"error" => {"message" => "Empty data and url", "http_code"=>404}}
277
+ end
278
+ main.results << {"id"=>row["id"], "internal_id"=>row["internal_id"], "result"=>result.to_json}
279
+ rescue => e
280
+ $stderr.print "Thread #{i} - Error in processing row #{row.inspect} - #{e}\n"
281
+ debug(e.backtrace.join("\n"))
282
+ sleep 1
283
+ ensure
284
+ main.mutex.synchronize{main.in_process -= 1}
285
+ main.close_if_needed(file)
286
+ end
287
+ end
288
+ end
289
+ end
290
+
291
+ retry_previous_queue # Retry all work from previous iteration before we start processing this one.
292
+ end
293
+
294
+ def debug(message)
295
+ if @debug
296
+ $stderr.print "#{Time.now} Cloudinary::Migrator #{message}\n"
297
+ end
298
+ end
299
+
300
+ def retry_previous_queue
301
+ last_id = 0
302
+ begin
303
+ prev_last_id, last_id = last_id, refill_queue(last_id)
304
+ end while last_id > prev_last_id
305
+ process_results
306
+ end
307
+
308
+ def process_all_pending
309
+ # Waiting for work to finish. While we are at it, process results.
310
+ while self.in_process > 0
311
+ process_results
312
+ sleep 0.1
313
+ end
314
+ # Make sure we processed all the results
315
+ process_results
316
+ end
317
+
318
+ def add_to_work_queue(row)
319
+ self.work << row
320
+ mutex.synchronize{self.in_process += 1}
321
+ end
322
+
323
+ def wait_for_queue
324
+ # Waiting f
325
+ while self.work.length > @max_processing
326
+ process_results
327
+ sleep 0.1
328
+ end
329
+ end
330
+
331
+ def self.sample
332
+ migrator = Cloudinary::Migrator.new(
333
+ :retrieve=>proc{|id| Post.find(id).data},
334
+ :complete=>proc{|id, result| a}
335
+ )
336
+
337
+ Post.find_each(:conditions=>["id > ?", migrator.max_given_id], :select=>"id") do
338
+ |post|
339
+ migrator.process(:id=>post.id, :public_id=>"post_#{post.id}")
340
+ end
341
+ migrator.done
342
+ end
343
+
344
+ def self.test
345
+ posts = {}
346
+ done = {}
347
+ migrator = Cloudinary::Migrator.new(
348
+ :retrieve=>proc{|id| posts[id]},
349
+ :complete=>proc{|id, result| $stderr.print "done #{id} #{result}\n"; done[id] = result}
350
+ )
351
+ start = migrator.max_given_id + 1
352
+ (start..1000).each{|i| posts[i] = "hello#{i}"}
353
+
354
+ posts.each do
355
+ |id, data|
356
+ migrator.process(:id=>id, :public_id=>"post_#{id}")
357
+ end
358
+ migrator.done
359
+ pp [done.length, start]
360
+ end
361
+ end
@@ -0,0 +1,114 @@
1
+ # Copyright Cloudinary
2
+ require 'rest_client'
3
+ require 'json'
4
+
5
+ class Cloudinary::Uploader
6
+
7
+ def self.upload(file, options={})
8
+ call_api("upload", options) do
9
+ params = {:timestamp=>Time.now.to_i,
10
+ :transformation => Cloudinary::Utils.generate_transformation_string(options),
11
+ :public_id=> options[:public_id],
12
+ :format=>options[:format],
13
+ :tags=>options[:tags] && Array(options[:tags]).join(",")}.reject{|k,v| v.blank?}
14
+ if options[:eager]
15
+ params[:eager] = options[:eager].map do
16
+ |transformation|
17
+ transformation, format = Array(transformation) # format is optional
18
+ [Cloudinary::Utils.generate_transformation_string(transformation.clone), format].compact.join("/")
19
+ end.join("|")
20
+ end
21
+ if file.respond_to?(:read) || file =~ /^https?:/
22
+ params[:file] = file
23
+ else
24
+ params[:file] = File.open(file, "rb")
25
+ end
26
+ [params, [:file]]
27
+ end
28
+ end
29
+
30
+ def self.generate_sprite(tag, options={})
31
+ version_store = options.delete(:version_store)
32
+
33
+ result = call_api("sprite", options) do
34
+ {
35
+ :timestamp=>Time.now.to_i,
36
+ :tag=>tag,
37
+ :transformation => Cloudinary::Utils.generate_transformation_string(options)
38
+ }
39
+ end
40
+
41
+ if version_store == :file && result && result["version"]
42
+ if defined?(Rails) && defined?(Rails.root)
43
+ FileUtils.mkdir_p("#{Rails.root}/tmp/cloudinary")
44
+ File.open("#{Rails.root}/tmp/cloudinary/cloudinary_sprite_#{tag}.version", "w"){|file| file.print result["version"].to_s}
45
+ end
46
+ end
47
+ return result
48
+ end
49
+
50
+ # options may include 'exclusive' (boolean) which causes clearing this tag from all other resources
51
+ def self.add_tag(tag, public_ids = [], options = {})
52
+ exclusive = options.delete(:exclusive)
53
+ command = exclusive ? "set_exclusive" : "add"
54
+ return self.call_tags_api(tag, command, public_ids, options)
55
+ end
56
+
57
+ def self.remove_tag(tag, public_ids = [], options = {})
58
+ return self.call_tags_api(tag, "remove", public_ids, options)
59
+ end
60
+
61
+ def self.replace_tag(tag, public_ids = [], options = {})
62
+ return self.call_tags_api(tag, "replace", public_ids, options)
63
+ end
64
+
65
+ private
66
+
67
+ def self.call_tags_api(tag, command, public_ids = [], options = {})
68
+ return call_api("tags", options) do
69
+ {
70
+ :timestamp=>Time.now.to_i,
71
+ :tag=>tag,
72
+ :public_ids => Array(public_ids),
73
+ :command => command
74
+ }
75
+ end
76
+ end
77
+
78
+ def self.call_api(action, options)
79
+ options = options.clone
80
+ return_error = options.delete(:return_error)
81
+ api_key = options[:api_key] || Cloudinary.config.api_key || raise("Must supply api_key")
82
+ api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise("Must supply api_secret")
83
+
84
+ params, non_signable = yield
85
+ non_signable ||= []
86
+
87
+ params[:signature] = Cloudinary::Utils.api_sign_request(params.reject{|k,v| non_signable.include?(k)}, api_secret)
88
+ params[:api_key] = api_key
89
+ cloudinary = options.delete(:upload_prefix) || Cloudinary.config.upload_prefix || "https://api.cloudinary.com"
90
+
91
+ resource_type = options.delete(:resource_type) || "image"
92
+ result = nil
93
+ cloud_name = Cloudinary.config.cloud_name || raise("Must supply cloud_name")
94
+ RestClient::Request.execute(:method => :post, :url => "#{cloudinary}/v1_1/#{cloud_name}/#{resource_type}/#{action}", :payload => params, :timeout=>60) do
95
+ |response, request, tmpresult|
96
+ raise "Server returned unexpected status code - #{response.code} - #{response.body}" if ![200,400,500].include?(response.code)
97
+ begin
98
+ result = Cloudinary::Utils.json_decode(response.body)
99
+ rescue => e
100
+ # Error is parsing json
101
+ raise "Error parsing server response (#{response.code}) - #{response.body}. Got - #{e}"
102
+ end
103
+ if result["error"]
104
+ if return_error
105
+ result["error"]["http_code"] = response.code
106
+ else
107
+ raise result["error"]["message"]
108
+ end
109
+ end
110
+ end
111
+
112
+ result
113
+ end
114
+ end
@@ -0,0 +1,98 @@
1
+ # Copyright Cloudinary
2
+ require 'digest/sha1'
3
+
4
+ class Cloudinary::Utils
5
+ SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"
6
+
7
+ # Warning: options are being destructively updated!
8
+ def self.generate_transformation_string(options={})
9
+ width = options[:width]
10
+ height = options[:height]
11
+ size = options.delete(:size)
12
+ width, height = size.split("x") if size
13
+ options.delete(:width) if width && width < 1
14
+ options.delete(:height) if height && height < 1
15
+
16
+ crop = options.delete(:crop)
17
+ width=height=nil if crop.nil?
18
+
19
+ gravity = options.delete(:gravity)
20
+ quality = options.delete(:quality)
21
+ named_transformation = Array(options.delete(:transformation)).join(".")
22
+ prefix = options.delete(:prefix)
23
+
24
+ params = {:w=>width, :h=>height, :t=>named_transformation, :c=>crop, :q=>quality, :g=>gravity, :p=>prefix}
25
+ transformation = params.reject{|k,v| v.blank?}.map{|k,v| [k.to_s, v]}.sort_by(&:first).map{|k,v| "#{k}_#{v}"}.join(",")
26
+ raw_transformation = options.delete(:raw_transformation)
27
+ transformation = [transformation, raw_transformation].reject(&:blank?).join(",")
28
+ transformation
29
+ end
30
+
31
+ def self.api_sign_request(params_to_sign, api_secret)
32
+ to_sign = params_to_sign.reject{|k,v| v.blank?}.map{|k,v| [k.to_s, v.is_a?(Array) ? v.join(",") : v]}.sort_by(&:first).map{|k,v| "#{k}=#{v}"}.join("&")
33
+ Digest::SHA1.hexdigest("#{to_sign}#{api_secret}")
34
+ end
35
+
36
+ # Warning: options are being destructively updated!
37
+ def self.cloudinary_url(source, options = {})
38
+ transformation = self.generate_transformation_string(options)
39
+
40
+ type = options.delete(:type) || :upload
41
+ resource_type = options.delete(:resource_type) || "image"
42
+ version = options.delete(:version)
43
+
44
+ format = options.delete(:format)
45
+ source = "#{source}.#{format}" if format
46
+
47
+ # Configuration options
48
+ # newsodrome.cloudinary.com, images.newsodrome.com, cloudinary.com/res/newsodrome, a9fj209daf.cloudfront.net
49
+ cloud_name = options.delete(:cloud_name) || Cloudinary.config.cloud_name || raise("Must supply cloud_name in tag or in configuration")
50
+
51
+ if cloud_name.start_with?("/")
52
+ prefix = "/res" + cloud_name
53
+ else
54
+ secure = options.delete(:secure) || Cloudinary.config.secure
55
+ private_cdn = options.delete(:private_cdn) || Cloudinary.config.private_cdn
56
+ secure_distribution = options.delete(:secure_distribution) || Cloudinary.config.secure_distribution
57
+ if secure && secure_distribution.nil?
58
+ if private_cdn
59
+ raise "secure_distribution not defined"
60
+ else
61
+ secure_distribution = SHARED_CDN
62
+ end
63
+ end
64
+
65
+ if secure
66
+ prefix = "https://#{secure_distribution}"
67
+ else
68
+ prefix = "http://#{private_cdn ? "#{cloud_name}-" : ""}res.cloudinary.com"
69
+ end
70
+ prefix += "/#{cloud_name}" if !private_cdn
71
+ end
72
+
73
+ source = prefix + "/" + [resource_type,
74
+ type, transformation, version ? "v#{version}" : nil,
75
+ source].reject(&:blank?).join("/").gsub("//", "/")
76
+ end
77
+
78
+ def self.random_public_id
79
+ (defined?(ActiveSupport::SecureRandom) ? ActiveSupport::SecureRandom : SecureRandom).base64(16).downcase.gsub(/[^a-z0-9]/, "")
80
+ end
81
+
82
+ @@json_decode = false
83
+ def self.json_decode(str)
84
+ if !@@json_decode
85
+ @@json_decode = true
86
+ begin
87
+ require 'json'
88
+ rescue LoadError
89
+ begin
90
+ require 'active_support/json'
91
+ rescue LoadError
92
+ raise "Please add the json gem or active_support to your Gemfile"
93
+ end
94
+ end
95
+ end
96
+ defined?(JSON) ? JSON.parse(str) : ActiveSupport::JSON.decode(str)
97
+ end
98
+ end
@@ -0,0 +1,4 @@
1
+ # Copyright Cloudinary
2
+ module Cloudinary
3
+ VERSION = "1.0.0"
4
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloudinary
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 1.0.0
6
+ platform: ruby
7
+ authors:
8
+ - Nadav Soferman
9
+ - Itai Lahan
10
+ - Tal Lev-Ami
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+
15
+ date: 2012-02-22 00:00:00 +02:00
16
+ default_executable:
17
+ dependencies:
18
+ - !ruby/object:Gem::Dependency
19
+ name: rest-client
20
+ prerelease: false
21
+ requirement: &id001 !ruby/object:Gem::Requirement
22
+ none: false
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: "0"
27
+ type: :runtime
28
+ version_requirements: *id001
29
+ description: Client library for easily using the Cloudinary service
30
+ email:
31
+ - nadav.soferman@cloudinary.com
32
+ - itai.lahan@cloudinary.com
33
+ - tal.levami@cloudinary.com
34
+ executables: []
35
+
36
+ extensions: []
37
+
38
+ extra_rdoc_files: []
39
+
40
+ files:
41
+ - .gitignore
42
+ - Gemfile
43
+ - README.md
44
+ - Rakefile
45
+ - cloudinary.gemspec
46
+ - lib/cloudinary.rb
47
+ - lib/cloudinary/blob.rb
48
+ - lib/cloudinary/carrier_wave.rb
49
+ - lib/cloudinary/downloader.rb
50
+ - lib/cloudinary/helper.rb
51
+ - lib/cloudinary/helpers.rb
52
+ - lib/cloudinary/migrator.rb
53
+ - lib/cloudinary/uploader.rb
54
+ - lib/cloudinary/utils.rb
55
+ - lib/cloudinary/version.rb
56
+ has_rdoc: true
57
+ homepage: http://cloudinary.com
58
+ licenses: []
59
+
60
+ post_install_message:
61
+ rdoc_options: []
62
+
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: "0"
77
+ requirements: []
78
+
79
+ rubyforge_project: cloudinary
80
+ rubygems_version: 1.6.2
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: Client library for easily using the Cloudinary service
84
+ test_files: []
85
+