cloudinary 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+