cloudinary 1.0.85 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- ZTNjODQ0YjA3MzJjMjNhMmEwNzIyZGExNTRiY2JhMGZjZGYyNTI0MQ==
5
- data.tar.gz: !binary |-
6
- MjhkM2NkNzk1YTExOWQ0ZjdjMTJlNWIzOTE2YzZhMjE2NWZhNDM5OQ==
2
+ SHA1:
3
+ metadata.gz: ed522de3b73b3847f23d46ee3ec165e7cdf20037
4
+ data.tar.gz: 2bda2d2079cf979dd6622e23ec4187dbcf56be34
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- YWM4OGJmYmQxNjc3OGIwYWQ5Y2U4OThhNGJkMDYxNTBlOWNlYzBjZDliYTBm
10
- OWIyZmY1MDI4NzlhODIzZmFmNWJkZDQyYTAyOWZmNzc0MzFjMWRlZDUyZGY5
11
- ZmYwYjQ5ODFiODM0Y2VlNjViN2JjOTJmODI2ODBiNzk4NWViMjk=
12
- data.tar.gz: !binary |-
13
- NTE4NDkyMGU0ZDk3MDNiNjA0OWMwZDZjNTU1OThjNjI3OTQ3Zjg1ZTkxM2U2
14
- MGFjODMxOTBiNDgyYmMyYTdmZmM1Y2FkMWM0ZGZmOWI2MjM3NjUzY2E4ZDhk
15
- YTc5OGY1OWIzNTIzYjcwYzRmNmMzZThiZjcwMTI4MTQyOTU3NmY=
6
+ metadata.gz: d75f033f6b0a02263f626b9f2f2c57de96fcf773eb455079fb647bf50c447f8280138eeb93298ba67a4d22f34c42f1f5ca29af1eb108dbe303fc19efd93b90ac
7
+ data.tar.gz: c815b8e4af15df396cc425ab505608c577c7f2616d76101041bf9c41f1561aea69fd7676a14294ef7df418d86c2375e044db6c7e6630b4d95b48c5f300e8fa30
data/.gitignore CHANGED
@@ -3,9 +3,9 @@
3
3
  /.config
4
4
  capybara-*.html
5
5
  .rspec
6
- log/
7
- db/*.sqlite3
8
- db/*.sqlite3-journal
6
+ /log
7
+ /db/*.sqlite3
8
+ /db/*.sqlite3-journal
9
9
  /public/system
10
10
  /coverage/
11
11
  /InstalledFiles
@@ -14,7 +14,7 @@ db/*.sqlite3-journal
14
14
  /test/tmp/
15
15
  /spec/tmp
16
16
  /test/version_tmp/
17
- tmp/
17
+ /tmp
18
18
  **.orig
19
19
  rerun.txt
20
20
  pickle-email-*.html
@@ -49,7 +49,7 @@ Gemfile.lock
49
49
  .rvmrc
50
50
 
51
51
  # if using bower-rails ignore default bower_components path bower.json files
52
- vendor/assets/bower_components
52
+ /vendor/assets/bower_components
53
53
  *.bowerrc
54
54
  bower.json
55
55
 
@@ -1,3 +1,16 @@
1
+ # Version 1.1.0 - 2015-04-21
2
+ * Pull request #136 - Update `process.rb` to ensure name value is an array.
3
+ * CarrierWave
4
+ * Store `resource_type` and `type` (aka `storage_type`) in carrierwave column for better support of non-image resource types and non-upload types
5
+ * only pass format to Cloudinary when explicitly requested
6
+ * Support disabling new extended identifier format
7
+ * Use upload endpoint instead of upload_chunked
8
+ * Remove `symoblize_keys` monkey patching
9
+ * Update Rspec dependency.
10
+ * Fix markup in the readme file.
11
+ * Add `.gitignore` to each sample project. Add files required for testing.
12
+ * Fix changelog format (missing newline)
13
+
1
14
  # Version 1.0.85 - 2015-04-08
2
15
  * Remove symoblize_keys intrusive implementation.
3
16
  * Use upload API endpoint instead of upload_chunked.
data/README.md CHANGED
@@ -12,13 +12,13 @@ Cloudinary provides URL and HTTP based APIs that can be easily integrated with a
12
12
  For Ruby on Rails, Cloudinary provides a GEM for simplifying the integration even further.
13
13
 
14
14
  ## Getting started guide
15
- ![](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **Take a look at our [Getting started guide of Ruby on Rails](http://cloudinary.com/documentation/rails_integration#getting_started_guide)**.
15
+ ![More](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **Take a look at our [Getting started guide of Ruby on Rails](http://cloudinary.com/documentation/rails_integration#getting_started_guide)**.
16
16
 
17
17
  ## Setup ######################################################################
18
18
 
19
19
  To install the Cloudinary Ruby GEM, run:
20
20
 
21
- $ gem install cloudinary
21
+ $ gem install cloudinary
22
22
 
23
23
  If you use Rails 3.x or higher, edit your `Gemfile`, add the following line and run `bundle install`
24
24
 
@@ -117,7 +117,7 @@ Same goes for Twitter:
117
117
 
118
118
  twitter_name_profile_image_tag("billclinton.jpg")
119
119
 
120
- ![](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **See [our documentation](http://cloudinary.com/documentation/rails_image_manipulation) for more information about displaying and transforming images in Rails**.
120
+ ![More](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **See [our documentation](http://cloudinary.com/documentation/rails_image_manipulation) for more information about displaying and transforming images in Rails**.
121
121
 
122
122
 
123
123
 
@@ -144,11 +144,14 @@ You can also specify your own public ID:
144
144
  http://res.cloudinary.com/demo/image/upload/sample_remote.jpg
145
145
 
146
146
 
147
- ![](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **See [our documentation](http://cloudinary.com/documentation/rails_image_upload) for plenty more options of uploading to the cloud from your Ruby code or directly from the browser**.
147
+ ![More](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **See [our documentation](http://cloudinary.com/documentation/rails_image_upload) for plenty more options of uploading to the cloud from your Ruby code or directly from the browser**.
148
148
 
149
149
 
150
150
  ### CarrierWave Integration
151
151
 
152
+ **Note:** Starting from version 1.1.0 the CarrierWave database format has changed to include the resource type and storage type. The new functionality
153
+ is backward compatible with the previous format. To use the old format override `use_extended_identifier?` in the Uploader and return `false`.
154
+
152
155
  Cloudinary's Ruby GEM includes an optional plugin for [CarrierWave](https://github.com/jnicklas/carrierwave). If you already use CarrierWave, simply include `Cloudinary::CarrierWave` to switch to cloud storage and image processing in the cloud.
153
156
 
154
157
  class PictureUploader < CarrierWave::Uploader::Base
@@ -156,7 +159,7 @@ Cloudinary's Ruby GEM includes an optional plugin for [CarrierWave](https://gith
156
159
  ...
157
160
  end
158
161
 
159
- ![](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **For more details on CarrierWave integration see [our documentation](http://cloudinary.com/documentation/rails_carrierwave)**.
162
+ ![More](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **For more details on CarrierWave integration see [our documentation](http://cloudinary.com/documentation/rails_carrierwave)**.
160
163
 
161
164
  We also published an interesting blog post about [Ruby on Rails image uploads with CarrierWave and Cloudinary](http://cloudinary.com/blog/ruby_on_rails_image_uploads_with_carrierwave_and_cloudinary).
162
165
 
data/Rakefile CHANGED
@@ -7,7 +7,7 @@ task :default => :spec
7
7
  Bundler::GemHelper.install_tasks
8
8
 
9
9
  task :fetch_assets do
10
- system "/bin/rm -rf vendor/assets; mkdir -p vendor/assets; cd vendor/assets; curl -L https://github.com/cloudinary/cloudinary_js/archive/1.0.23.tar.gz | tar zxvf - --strip=1"
10
+ system "/bin/rm -rf vendor/assets; mkdir -p vendor/assets; cd vendor/assets; curl -L https://github.com/cloudinary/cloudinary_js/tarball/master | tar zxvf - --strip=1"
11
11
  system "mkdir -p vendor/assets/javascripts; mv vendor/assets/js vendor/assets/javascripts/cloudinary"
12
12
  File.open("vendor/assets/javascripts/cloudinary/index.js", "w") do
13
13
  |f|
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
21
21
  s.require_paths = ["lib"]
22
22
 
23
23
  s.add_dependency "aws_cf_signer"
24
- s.add_development_dependency "rspec", '>=2.11'
24
+ s.add_development_dependency "rspec", '>=3.2'
25
25
  s.add_development_dependency "rspec-rails"
26
26
 
27
27
  if RUBY_VERSION > "1.9"
@@ -135,6 +135,11 @@ module Cloudinary::CarrierWave
135
135
  def auto_rename_preloaded?
136
136
  true
137
137
  end
138
+
139
+ # Use extended identifier format that includes resource type and storage type.
140
+ def use_extended_identifier?
141
+ true
142
+ end
138
143
 
139
144
  class CloudinaryFile
140
145
  attr_reader :identifier, :public_id, :filename, :format, :version, :storage_type, :resource_type
@@ -142,7 +147,12 @@ module Cloudinary::CarrierWave
142
147
  @uploader = uploader
143
148
  @identifier = identifier
144
149
 
145
- if @identifier.match(%r(^v([0-9]+)/(.*)))
150
+ if @identifier.match(%r(^(image|raw|video)/(upload|private|authenticated)(?:/v([0-9]+))?/(.*)))
151
+ @resource_type = $1
152
+ @storage_type = $2
153
+ @version = $3.presence
154
+ @filename = $4
155
+ elsif @identifier.match(%r(^v([0-9]+)/(.*)))
146
156
  @version = $1
147
157
  @filename = $2
148
158
  else
@@ -150,8 +160,8 @@ module Cloudinary::CarrierWave
150
160
  @version = nil
151
161
  end
152
162
 
153
- @storage_type = uploader.class.storage_type
154
- @resource_type = Cloudinary::Utils.resource_type_for_format(@filename)
163
+ @storage_type ||= uploader.class.storage_type
164
+ @resource_type ||= Cloudinary::Utils.resource_type_for_format(@filename)
155
165
  @public_id, @format = Cloudinary::PreloadedFile.split_format(@filename)
156
166
  end
157
167
 
@@ -180,8 +190,12 @@ module Cloudinary::CarrierWave
180
190
  "png"
181
191
  end
182
192
 
193
+ def storage_type
194
+ @file.respond_to?(:storage_type) ? @file.storage_type : self.class.storage_type
195
+ end
196
+
183
197
  def resource_type
184
- Cloudinary::Utils.resource_type_for_format(requested_format || original_filename || default_format)
198
+ @file.respond_to?(:resource_type) ? @file.resource_type : Cloudinary::Utils.resource_type_for_format(requested_format || original_filename || default_format)
185
199
  end
186
200
 
187
201
  # For the given methods - versions should call the main uploader method
@@ -105,7 +105,7 @@ module Cloudinary::CarrierWave
105
105
  end
106
106
  else
107
107
  if args.blank?
108
- send(name).each do
108
+ Array(send(name)).each do
109
109
  |attr, value|
110
110
  set_or_yell(@transformation, attr, value)
111
111
  end
@@ -11,11 +11,11 @@ class Cloudinary::CarrierWave::Storage < ::CarrierWave::Storage::Abstract
11
11
  @stored_version = file.version
12
12
  uploader.rename(nil, true)
13
13
  else
14
- store_cloudinary_identifier(file.version, file.filename)
14
+ store_cloudinary_identifier(file.version, file.filename, file.resource_type, file.type)
15
15
  end
16
- return
16
+ return # Nothing to do
17
17
  when Cloudinary::CarrierWave::CloudinaryFile, Cloudinary::CarrierWave::StoredFile
18
- return nil # Nothing to do
18
+ return # Nothing to do
19
19
  when Cloudinary::CarrierWave::RemoteFile
20
20
  data = file.uri.to_s
21
21
  else
@@ -26,7 +26,7 @@ class Cloudinary::CarrierWave::Storage < ::CarrierWave::Storage::Abstract
26
26
  # This is the toplevel, need to upload the actual file.
27
27
  params = uploader.transformation.dup
28
28
  params[:return_error] = true
29
- params[:format] = uploader.format
29
+ params[:format] = uploader.requested_format
30
30
  params[:public_id] = uploader.my_public_id
31
31
  uploader.versions.values.each(&:tags) # Validate no tags in versions
32
32
  params[:tags] = uploader.tags if uploader.tags
@@ -42,7 +42,7 @@ class Cloudinary::CarrierWave::Storage < ::CarrierWave::Storage::Abstract
42
42
 
43
43
  if uploader.metadata["version"]
44
44
  filename = [uploader.metadata["public_id"], uploader.metadata["format"]].reject(&:blank?).join(".")
45
- store_cloudinary_identifier(uploader.metadata["version"], filename)
45
+ store_cloudinary_identifier(uploader.metadata["version"], filename, uploader.metadata["resource_type"], uploader.metadata["type"])
46
46
  end
47
47
  # Will throw an exception on error
48
48
  else
@@ -52,24 +52,18 @@ class Cloudinary::CarrierWave::Storage < ::CarrierWave::Storage::Abstract
52
52
  nil
53
53
  end
54
54
 
55
- # @deprecated For backward compatibility
56
- def store_cloudinary_version(version)
57
- if identifier.match(%r(^(v[0-9]+)/(.*)))
58
- filename = $2
59
- else
60
- filename = identifier
61
- end
62
-
63
- store_cloudinary_identifier(version, filename)
64
- end
65
-
66
55
  # Updates the model mounter identifier with version information.
67
56
  #
68
57
  # Carrierwave uses hooks when integrating with ORMs so it's important to
69
58
  # update the identifier in a way that does not trigger hooks again or else
70
59
  # you'll get stuck in a loop.
71
- def store_cloudinary_identifier(version, filename)
60
+ def store_cloudinary_identifier(version, filename, resource_type=nil, type=nil)
72
61
  name = "v#{version}/#{filename}"
62
+ if uploader.use_extended_identifier?
63
+ resource_type ||= uploader.resource_type || "image"
64
+ type ||= uploader.storage_type || "upload"
65
+ name = "#{resource_type}/#{type}/#{name}"
66
+ end
73
67
  model_class = uploader.model.class
74
68
  column = uploader.model.send(:_mounter, uploader.mounted_as).send(:serialization_column)
75
69
  if defined?(ActiveRecord::Base) && uploader.model.is_a?(ActiveRecord::Base)
@@ -1,551 +1,554 @@
1
- # Copyright Cloudinary
2
- require 'digest/sha1'
3
- require 'zlib'
4
- require 'uri'
5
- require 'aws_cf_signer'
6
-
7
- class Cloudinary::Utils
8
- # @deprecated Use Cloudinary::SHARED_CDN
9
- SHARED_CDN = Cloudinary::SHARED_CDN
10
- DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION = {:width => :auto, :crop => :limit}
11
-
12
- # Warning: options are being destructively updated!
13
- def self.generate_transformation_string(options={})
14
- if options.is_a?(Array)
15
- return options.map{|base_transformation| generate_transformation_string(base_transformation.clone)}.join("/")
16
- end
17
- # Symbolize keys
18
- options.keys.each do |key|
19
- options[(key.to_sym rescue key)] = options.delete(key)
20
- end
21
-
22
- responsive_width = config_option_consume(options, :responsive_width)
23
- size = options.delete(:size)
24
- options[:width], options[:height] = size.split("x") if size
25
- width = options[:width]
26
- width = width.to_s if width.is_a?(Symbol)
27
- height = options[:height]
28
- has_layer = options[:overlay].present? || options[:underlay].present?
29
-
30
- crop = options.delete(:crop)
31
- angle = build_array(options.delete(:angle)).join(".")
32
-
33
- no_html_sizes = has_layer || angle.present? || crop.to_s == "fit" || crop.to_s == "limit" || crop.to_s == "lfill"
34
- options.delete(:width) if width && (width.to_f < 1 || no_html_sizes || width == "auto" || responsive_width)
35
- options.delete(:height) if height && (height.to_f < 1 || no_html_sizes || responsive_width)
36
-
37
- width=height=nil if crop.nil? && !has_layer && width != "auto"
38
-
39
- background = options.delete(:background)
40
- background = background.sub(/^#/, 'rgb:') if background
41
-
42
- color = options.delete(:color)
43
- color = color.sub(/^#/, 'rgb:') if color
44
-
45
- base_transformations = build_array(options.delete(:transformation))
46
- if base_transformations.any?{|base_transformation| base_transformation.is_a?(Hash)}
47
- base_transformations = base_transformations.map do
48
- |base_transformation|
49
- base_transformation.is_a?(Hash) ? generate_transformation_string(base_transformation.clone) : generate_transformation_string(:transformation=>base_transformation)
50
- end
51
- else
52
- named_transformation = base_transformations.join(".")
53
- base_transformations = []
54
- end
55
-
56
- effect = options.delete(:effect)
57
- effect = Array(effect).flatten.join(":") if effect.is_a?(Array) || effect.is_a?(Hash)
58
-
59
- border = options.delete(:border)
60
- if border.is_a?(Hash)
61
- border = "#{border[:width] || 2}px_solid_#{(border[:color] || "black").sub(/^#/, 'rgb:')}"
62
- elsif border.to_s =~ /^\d+$/ # fallback to html border attribute
63
- options[:border] = border
64
- border = nil
65
- end
66
- flags = build_array(options.delete(:flags)).join(".")
67
- dpr = config_option_consume(options, :dpr)
68
-
69
- if options.include? :offset
70
- options[:start_offset], options[:end_offset] = split_range options.delete(:offset)
71
- end
72
-
73
- params = {
74
- :a => angle,
75
- :b => background,
76
- :bo => border,
77
- :c => crop,
78
- :co => color,
79
- :dpr => dpr,
80
- :e => effect,
81
- :fl => flags,
82
- :h => height,
83
- :t => named_transformation,
84
- :w => width
85
- }
86
- {
87
- :ac => :audio_codec,
88
- :br => :bit_rate,
89
- :cs => :color_space,
90
- :d => :default_image,
91
- :dl => :delay,
92
- :dn => :density,
93
- :du => :duration,
94
- :eo => :end_offset,
95
- :f => :fetch_format,
96
- :g => :gravity,
97
- :l => :overlay,
98
- :o => :opacity,
99
- :p => :prefix,
100
- :pg => :page,
101
- :q => :quality,
102
- :r => :radius,
103
- :af => :audio_frequency,
104
- :so => :start_offset,
105
- :u => :underlay,
106
- :vc => :video_codec,
107
- :vs => :video_sampling,
108
- :x => :x,
109
- :y => :y,
110
- :z => :zoom
111
- }.each do
112
- |param, option|
113
- params[param] = options.delete(option)
114
- end
115
-
116
- params[:vc] = process_video_params params[:vc] if params[:vc].present?
117
- [:so, :eo, :du].each do |range_value|
118
- params[range_value] = norm_range_value params[range_value] if params[range_value].present?
119
- end
120
-
121
- transformation = params.reject{|_k,v| v.blank?}.map{|k,v| "#{k}_#{v}"}.sort.join(",")
122
- raw_transformation = options.delete(:raw_transformation)
123
- transformation = [transformation, raw_transformation].reject(&:blank?).join(",")
124
- transformations = base_transformations << transformation
125
- if responsive_width
126
- responsive_width_transformation = Cloudinary.config.responsive_width_transformation || DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION
127
- transformations << generate_transformation_string(responsive_width_transformation.clone)
128
- end
129
-
130
- if width.to_s == "auto" || responsive_width
131
- options[:responsive] = true
132
- end
133
- if dpr.to_s == "auto"
134
- options[:hidpi] = true
135
- end
136
-
137
- transformations.reject(&:blank?).join("/")
138
- end
139
-
140
- def self.api_string_to_sign(params_to_sign)
141
- params_to_sign.map{|k,v| [k.to_s, v.is_a?(Array) ? v.join(",") : v]}.reject{|k,v| v.nil? || v == ""}.sort_by(&:first).map{|k,v| "#{k}=#{v}"}.join("&")
142
- end
143
-
144
- def self.api_sign_request(params_to_sign, api_secret)
145
- to_sign = api_string_to_sign(params_to_sign)
146
- Digest::SHA1.hexdigest("#{to_sign}#{api_secret}")
147
- end
148
-
149
- # Warning: options are being destructively updated!
150
- def self.unsigned_download_url(source, options = {})
151
-
152
- type = options.delete(:type)
153
-
154
- options[:fetch_format] ||= options.delete(:format) if type == :fetch
155
- transformation = self.generate_transformation_string(options)
156
-
157
- resource_type = options.delete(:resource_type) || "image"
158
- version = options.delete(:version)
159
- format = options.delete(:format)
160
- cloud_name = config_option_consume(options, :cloud_name) || raise(CloudinaryException, "Must supply cloud_name in tag or in configuration")
161
-
162
- secure = options.delete(:secure)
163
- ssl_detected = options.delete(:ssl_detected)
164
- secure = ssl_detected || Cloudinary.config.secure if secure.nil?
165
- private_cdn = config_option_consume(options, :private_cdn)
166
- secure_distribution = config_option_consume(options, :secure_distribution)
167
- cname = config_option_consume(options, :cname)
168
- shorten = config_option_consume(options, :shorten)
169
- force_remote = options.delete(:force_remote)
170
- cdn_subdomain = config_option_consume(options, :cdn_subdomain)
171
- secure_cdn_subdomain = config_option_consume(options, :secure_cdn_subdomain)
172
- sign_url = config_option_consume(options, :sign_url)
173
- secret = config_option_consume(options, :api_secret)
174
- sign_version = config_option_consume(options, :sign_version) # Deprecated behavior
175
- url_suffix = options.delete(:url_suffix)
176
- use_root_path = config_option_consume(options, :use_root_path)
177
-
178
- raise(CloudinaryException, "URL Suffix only supported in private CDN") if url_suffix.present? and not private_cdn
179
-
180
- original_source = source
181
- return original_source if source.blank?
182
- if defined?(CarrierWave::Uploader::Base) && source.is_a?(CarrierWave::Uploader::Base)
183
- source = format.blank? ? source.filename : source.full_public_id
184
- end
185
- source = source.to_s
186
- if !force_remote
187
- return original_source if (type.nil? || type == :asset) && source.match(%r(^https?:/)i)
188
- if source.start_with?("/")
189
- if source.start_with?("/images/")
190
- source = source.sub(%r(/images/), '')
191
- else
192
- return original_source
193
- end
194
- end
195
- @metadata ||= defined?(Cloudinary::Static) ? Cloudinary::Static.metadata : {}
196
- if type == :asset && @metadata["images/#{source}"]
197
- return original_source if !Cloudinary.config.static_image_support
198
- source = @metadata["images/#{source}"]["public_id"]
199
- source += File.extname(original_source) if !format
200
- elsif type == :asset
201
- return original_source # requested asset, but no metadata - probably local file. return.
202
- end
203
- end
204
-
205
- resource_type, type = finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
206
- source, source_to_sign = finalize_source(source, format, url_suffix)
207
-
208
- version ||= 1 if source_to_sign.include?("/") and !source_to_sign.match(/^v[0-9]+/) and !source_to_sign.match(/^https?:\//)
209
- version &&= "v#{version}"
210
-
211
- transformation = transformation.gsub(%r(([^:])//), '\1/')
212
- if sign_url
213
- to_sign = [transformation, sign_version && version, source_to_sign].reject(&:blank?).join("/")
214
- signature = 's--' + Base64.urlsafe_encode64(Digest::SHA1.digest(to_sign + secret))[0,8] + '--'
215
- end
216
-
217
- prefix = unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
218
- source = [prefix, resource_type, type, signature, transformation, version, source].reject(&:blank?).join("/")
219
- end
220
-
221
- def self.finalize_source(source, format, url_suffix)
222
- source = source.gsub(%r(([^:])//), '\1/')
223
- if source.match(%r(^https?:/)i)
224
- source = smart_escape(source)
225
- source_to_sign = source
226
- else
227
- source = smart_escape(URI.decode(source))
228
- source_to_sign = source
229
- unless url_suffix.blank?
230
- raise(CloudinaryException, "url_suffix should not include . or /") if url_suffix.match(%r([\./]))
231
- source = "#{source}/#{url_suffix}"
232
- end
233
- if !format.blank?
234
- source = "#{source}.#{format}"
235
- source_to_sign = "#{source_to_sign}.#{format}"
236
- end
237
- end
238
- [source, source_to_sign]
239
- end
240
-
241
- def self.finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
242
- type ||= :upload
243
- if !url_suffix.blank?
244
- if resource_type.to_s == "image" && type.to_s == "upload"
245
- resource_type = "images"
246
- type = nil
247
- elsif resource_type.to_s == "raw" && type.to_s == "upload"
248
- resource_type = "files"
249
- type = nil
250
- else
251
- raise(CloudinaryException, "URL Suffix only supported for image/upload and raw/upload")
252
- end
253
- end
254
- if use_root_path
255
- if (resource_type.to_s == "image" && type.to_s == "upload") || (resource_type.to_s == "images" && type.blank?)
256
- resource_type = nil
257
- type = nil
258
- else
259
- raise(CloudinaryException, "Root path only supported for image/upload")
260
- end
261
- end
262
- if shorten && resource_type.to_s == "image" && type.to_s == "upload"
263
- resource_type = "iu"
264
- type = nil
265
- end
266
- [resource_type, type]
267
- end
268
-
269
- # cdn_subdomain and secure_cdn_subdomain
270
- # 1) Customers in shared distribution (e.g. res.cloudinary.com)
271
- # if cdn_domain is true uses res-[1-5].cloudinary.com for both http and https. Setting secure_cdn_subdomain to false disables this for https.
272
- # 2) Customers with private cdn
273
- # if cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for http
274
- # if secure_cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for https (please contact support if you require this)
275
- # 3) Customers with cname
276
- # if cdn_domain is true uses a[1-5].cname for http. For https, uses the same naming scheme as 1 for shared distribution and as 2 for private distribution.
277
- def self.unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
278
- return "/res#{cloud_name}" if cloud_name.start_with?("/") # For development
279
-
280
- shared_domain = !private_cdn
281
-
282
- if secure
283
- if secure_distribution.nil? || secure_distribution == Cloudinary::OLD_AKAMAI_SHARED_CDN
284
- secure_distribution = private_cdn ? "#{cloud_name}-res.cloudinary.com" : Cloudinary::SHARED_CDN
285
- end
286
- shared_domain ||= secure_distribution == Cloudinary::SHARED_CDN
287
- secure_cdn_subdomain = cdn_subdomain if secure_cdn_subdomain.nil? && shared_domain
288
-
289
- if secure_cdn_subdomain
290
- secure_distribution = secure_distribution.gsub('res.cloudinary.com', "res-#{(Zlib::crc32(source) % 5) + 1}.cloudinary.com")
291
- end
292
-
293
- prefix = "https://#{secure_distribution}"
294
- elsif cname
295
- subdomain = cdn_subdomain ? "a#{(Zlib::crc32(source) % 5) + 1}." : ""
296
- prefix = "http://#{subdomain}#{cname}"
297
- else
298
- host = [private_cdn ? "#{cloud_name}-" : "", "res", cdn_subdomain ? "-#{(Zlib::crc32(source) % 5) + 1}" : "", ".cloudinary.com"].join
299
- prefix = "http://#{host}"
300
- end
301
- prefix += "/#{cloud_name}" if shared_domain
302
-
303
- prefix
304
- end
305
-
306
- def self.cloudinary_api_url(action = 'upload', options = {})
307
- cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || "https://api.cloudinary.com"
308
- cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise(CloudinaryException, "Must supply cloud_name")
309
- resource_type = options[:resource_type] || "image"
310
- return [cloudinary, "v1_1", cloud_name, resource_type, action].join("/")
311
- end
312
-
313
- def self.sign_request(params, options={})
314
- api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
315
- api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
316
- params = params.reject{|k, v| self.safe_blank?(v)}
317
- params[:signature] = Cloudinary::Utils.api_sign_request(params, api_secret)
318
- params[:api_key] = api_key
319
- params
320
- end
321
-
322
- def self.private_download_url(public_id, format, options = {})
323
- cloudinary_params = sign_request({
324
- :timestamp=>Time.now.to_i,
325
- :public_id=>public_id,
326
- :format=>format,
327
- :type=>options[:type],
328
- :attachment=>options[:attachment],
329
- :expires_at=>options[:expires_at] && options[:expires_at].to_i
330
- }, options)
331
-
332
- return Cloudinary::Utils.cloudinary_api_url("download", options) + "?" + cloudinary_params.to_query
333
- end
334
-
335
- def self.zip_download_url(tag, options = {})
336
- cloudinary_params = sign_request({:timestamp=>Time.now.to_i, :tag=>tag, :transformation=>generate_transformation_string(options)}, options)
337
- return Cloudinary::Utils.cloudinary_api_url("download_tag.zip", options) + "?" + cloudinary_params.to_query
338
- end
339
-
340
- def self.signed_download_url(public_id, options = {})
341
- aws_private_key_path = options[:aws_private_key_path] || Cloudinary.config.aws_private_key_path || raise(CloudinaryException, "Must supply aws_private_key_path")
342
- aws_key_pair_id = options[:aws_key_pair_id] || Cloudinary.config.aws_key_pair_id || raise(CloudinaryException, "Must supply aws_key_pair_id")
343
- authenticated_distribution = options[:authenticated_distribution] || Cloudinary.config.authenticated_distribution || raise(CloudinaryException, "Must supply authenticated_distribution")
344
- @signers ||= Hash.new{|h,k| path, id = k; h[k] = AwsCfSigner.new(path, id)}
345
- signer = @signers[[aws_private_key_path, aws_key_pair_id]]
346
- url = Cloudinary::Utils.unsigned_download_url(public_id, {:type=>:authenticated}.merge(options).merge(:secure=>true, :secure_distribution=>authenticated_distribution, :private_cdn=>true))
347
- expires_at = options[:expires_at] || (Time.now+3600)
348
- signer.sign(url, :ending => expires_at)
349
- end
350
-
351
- def self.cloudinary_url(public_id, options = {})
352
- if options[:type].to_s == 'authenticated' && !options[:sign_url]
353
- result = signed_download_url(public_id, options)
354
- else
355
- result = unsigned_download_url(public_id, options)
356
- end
357
- return result
358
- end
359
-
360
- def self.asset_file_name(path)
361
- data = Cloudinary.app_root.join(path).read(:mode=>"rb")
362
- ext = path.extname
363
- md5 = Digest::MD5.hexdigest(data)
364
- public_id = "#{path.basename(ext)}-#{md5}"
365
- "#{public_id}#{ext}"
366
- end
367
-
368
- # Based on CGI::unescape. In addition does not escape / :
369
- def self.smart_escape(string)
370
- string.gsub(/([^a-zA-Z0-9_.\-\/:]+)/) do
371
- '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
372
- end
373
- end
374
-
375
- def self.random_public_id
376
- sr = defined?(ActiveSupport::SecureRandom) ? ActiveSupport::SecureRandom : SecureRandom
377
- sr.base64(20).downcase.gsub(/[^a-z0-9]/, "").sub(/^[0-9]+/, '')[0,20]
378
- end
379
-
380
- def self.signed_preloaded_image(result)
381
- "#{result["resource_type"]}/#{result["type"] || "upload"}/v#{result["version"]}/#{[result["public_id"], result["format"]].reject(&:blank?).join(".")}##{result["signature"]}"
382
- end
383
-
384
- @@json_decode = false
385
- def self.json_decode(str)
386
- if !@@json_decode
387
- @@json_decode = true
388
- begin
389
- require 'json'
390
- rescue LoadError
391
- begin
392
- require 'active_support/json'
393
- rescue LoadError
394
- raise LoadError, "Please add the json gem or active_support to your Gemfile"
395
- end
396
- end
397
- end
398
- defined?(JSON) ? JSON.parse(str) : ActiveSupport::JSON.decode(str)
399
- end
400
-
401
- def self.build_array(array)
402
- case array
403
- when Array then array
404
- when nil then []
405
- else [array]
406
- end
407
- end
408
-
409
- def self.encode_hash(hash)
410
- case hash
411
- when Hash then hash.map{|k,v| "#{k}=#{v}"}.join("|")
412
- when nil then ""
413
- else hash
414
- end
415
- end
416
-
417
- def self.encode_double_array(array)
418
- array = build_array(array)
419
- if array.length > 0 && array[0].is_a?(Array)
420
- return array.map{|a| build_array(a).join(",")}.join("|")
421
- else
422
- return array.join(",")
423
- end
424
- end
425
-
426
- IMAGE_FORMATS = %w(bmp png tif tiff jpg jpeg gif pdf ico eps jpc jp2 psd)
427
-
428
- def self.supported_image_format?(format)
429
- format = format.to_s.downcase
430
- extension = format =~ /\./ ? format.split('.').last : format
431
- IMAGE_FORMATS.include?(extension)
432
- end
433
-
434
- def self.resource_type_for_format(format)
435
- self.supported_image_format?(format) ? 'image' : 'raw'
436
- end
437
-
438
- def self.config_option_consume(options, option_name, default_value = nil)
439
- return options.delete(option_name) if options.include?(option_name)
440
- return Cloudinary.config.send(option_name) || default_value
441
- end
442
-
443
- def self.as_bool(value)
444
- case value
445
- when nil then nil
446
- when String then value.downcase == "true" || value == "1"
447
- when TrueClass then true
448
- when FalseClass then false
449
- when Fixnum then value != 0
450
- when Symbol then value == :true
451
- else
452
- raise "Invalid boolean value #{value} of type #{value.class}"
453
- end
454
- end
455
-
456
- def self.as_safe_bool(value)
457
- case as_bool(value)
458
- when nil then nil
459
- when TrueClass then 1
460
- when FalseClass then 0
461
- end
462
- end
463
-
464
- def self.safe_blank?(value)
465
- value.nil? || value == "" || value == []
466
- end
467
-
468
- private
469
- def self.number_pattern
470
- "([0-9]*)\\.([0-9]+)|([0-9]+)"
471
- end
472
-
473
- def self.offset_any_pattern
474
- "(#{number_pattern})([%pP])?"
475
- end
476
-
477
- def self.offset_any_pattern_re
478
- /((([0-9]*)\.([0-9]+)|([0-9]+))([%pP])?)\.\.((([0-9]*)\.([0-9]+)|([0-9]+))([%pP])?)/
479
- end
480
-
481
- # Split a range into the start and end values
482
- def self.split_range(range) # :nodoc:
483
- case range
484
- when Range
485
- [range.first, range.last]
486
- when String
487
- range.split ".." if offset_any_pattern_re =~ range
488
- when Array
489
- [range.first, range.last]
490
- else
491
- nil
492
- end
493
- end
494
-
495
- # Normalize an offset value
496
- # @param [String] value a decimal value which may have a 'p' or '%' postfix. E.g. '35%', '0.4p'
497
- # @return [Object|String] a normalized String of the input value if possible otherwise the value itself
498
- def self.norm_range_value(value) # :nodoc:
499
- offset = /^#{offset_any_pattern}$/.match( value.to_s)
500
- if offset
501
- modifier = offset[5].present? ? 'p' : ''
502
- value = "#{offset[1]}#{modifier}"
503
- end
504
- value
505
- end
506
-
507
- # A video codec parameter can be either a String or a Hash.
508
- #
509
- # @param [Object] param <code>vc_<codec>[ : <profile> : [<level>]]</code>
510
- # or <code>{ codec: 'h264', profile: 'basic', level: '3.1' }</code>
511
- # @return [String] <code><codec> : <profile> : [<level>]]</code> if a Hash was provided
512
- # or the param if a String was provided.
513
- # Returns NIL if param is not a Hash or String
514
- def self.process_video_params(param)
515
- case param
516
- when Hash
517
- video = ""
518
- if param.has_key? :codec
519
- video = param[:codec]
520
- if param.has_key? :profile
521
- video.concat ":" + param[:profile]
522
- if param.has_key? :level
523
- video.concat ":" + param[:level]
524
- end
525
- end
526
- end
527
- video
528
- when String
529
- param
530
- else
531
- nil
532
- end
533
- end
534
-
535
- def self.deep_symbolize_keys(object)
536
- case object
537
- when Hash
538
- result = {}
539
- object.each do |key, value|
540
- key = key.to_sym rescue key
541
- result[key] = deep_symbolize_keys(value)
542
- end
543
- result
544
- when Array
545
- object.map{|e| deep_symbolize_keys(e)}
546
- else
547
- object
548
- end
549
- end
550
-
551
- end
1
+ # Copyright Cloudinary
2
+ require 'digest/sha1'
3
+ require 'zlib'
4
+ require 'uri'
5
+ require 'aws_cf_signer'
6
+
7
+ class Cloudinary::Utils
8
+ # @deprecated Use Cloudinary::SHARED_CDN
9
+ SHARED_CDN = Cloudinary::SHARED_CDN
10
+ DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION = {:width => :auto, :crop => :limit}
11
+
12
+ # Warning: options are being destructively updated!
13
+ def self.generate_transformation_string(options={})
14
+ if options.is_a?(Array)
15
+ return options.map{|base_transformation| generate_transformation_string(base_transformation.clone)}.join("/")
16
+ end
17
+ # Symbolize keys
18
+ options.keys.each do |key|
19
+ options[(key.to_sym rescue key)] = options.delete(key)
20
+ end
21
+
22
+ responsive_width = config_option_consume(options, :responsive_width)
23
+ size = options.delete(:size)
24
+ options[:width], options[:height] = size.split("x") if size
25
+ width = options[:width]
26
+ width = width.to_s if width.is_a?(Symbol)
27
+ height = options[:height]
28
+ has_layer = options[:overlay].present? || options[:underlay].present?
29
+
30
+ crop = options.delete(:crop)
31
+ angle = build_array(options.delete(:angle)).join(".")
32
+
33
+ no_html_sizes = has_layer || angle.present? || crop.to_s == "fit" || crop.to_s == "limit" || crop.to_s == "lfill"
34
+ options.delete(:width) if width && (width.to_f < 1 || no_html_sizes || width == "auto" || responsive_width)
35
+ options.delete(:height) if height && (height.to_f < 1 || no_html_sizes || responsive_width)
36
+
37
+ width=height=nil if crop.nil? && !has_layer && width != "auto"
38
+
39
+ background = options.delete(:background)
40
+ background = background.sub(/^#/, 'rgb:') if background
41
+
42
+ color = options.delete(:color)
43
+ color = color.sub(/^#/, 'rgb:') if color
44
+
45
+ base_transformations = build_array(options.delete(:transformation))
46
+ if base_transformations.any?{|base_transformation| base_transformation.is_a?(Hash)}
47
+ base_transformations = base_transformations.map do
48
+ |base_transformation|
49
+ base_transformation.is_a?(Hash) ? generate_transformation_string(base_transformation.clone) : generate_transformation_string(:transformation=>base_transformation)
50
+ end
51
+ else
52
+ named_transformation = base_transformations.join(".")
53
+ base_transformations = []
54
+ end
55
+
56
+ effect = options.delete(:effect)
57
+ effect = Array(effect).flatten.join(":") if effect.is_a?(Array) || effect.is_a?(Hash)
58
+
59
+ border = options.delete(:border)
60
+ if border.is_a?(Hash)
61
+ border = "#{border[:width] || 2}px_solid_#{(border[:color] || "black").sub(/^#/, 'rgb:')}"
62
+ elsif border.to_s =~ /^\d+$/ # fallback to html border attribute
63
+ options[:border] = border
64
+ border = nil
65
+ end
66
+ flags = build_array(options.delete(:flags)).join(".")
67
+ dpr = config_option_consume(options, :dpr)
68
+
69
+ if options.include? :offset
70
+ options[:start_offset], options[:end_offset] = split_range options.delete(:offset)
71
+ end
72
+
73
+ params = {
74
+ :a => angle,
75
+ :b => background,
76
+ :bo => border,
77
+ :c => crop,
78
+ :co => color,
79
+ :dpr => dpr,
80
+ :e => effect,
81
+ :fl => flags,
82
+ :h => height,
83
+ :t => named_transformation,
84
+ :w => width
85
+ }
86
+ {
87
+ :ac => :audio_codec,
88
+ :br => :bit_rate,
89
+ :cs => :color_space,
90
+ :d => :default_image,
91
+ :dl => :delay,
92
+ :dn => :density,
93
+ :du => :duration,
94
+ :eo => :end_offset,
95
+ :f => :fetch_format,
96
+ :g => :gravity,
97
+ :l => :overlay,
98
+ :o => :opacity,
99
+ :p => :prefix,
100
+ :pg => :page,
101
+ :q => :quality,
102
+ :r => :radius,
103
+ :af => :audio_frequency,
104
+ :so => :start_offset,
105
+ :u => :underlay,
106
+ :vc => :video_codec,
107
+ :vs => :video_sampling,
108
+ :x => :x,
109
+ :y => :y,
110
+ :z => :zoom
111
+ }.each do
112
+ |param, option|
113
+ params[param] = options.delete(option)
114
+ end
115
+
116
+ params[:vc] = process_video_params params[:vc] if params[:vc].present?
117
+ [:so, :eo, :du].each do |range_value|
118
+ params[range_value] = norm_range_value params[range_value] if params[range_value].present?
119
+ end
120
+
121
+ transformation = params.reject{|_k,v| v.blank?}.map{|k,v| "#{k}_#{v}"}.sort.join(",")
122
+ raw_transformation = options.delete(:raw_transformation)
123
+ transformation = [transformation, raw_transformation].reject(&:blank?).join(",")
124
+ transformations = base_transformations << transformation
125
+ if responsive_width
126
+ responsive_width_transformation = Cloudinary.config.responsive_width_transformation || DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION
127
+ transformations << generate_transformation_string(responsive_width_transformation.clone)
128
+ end
129
+
130
+ if width.to_s == "auto" || responsive_width
131
+ options[:responsive] = true
132
+ end
133
+ if dpr.to_s == "auto"
134
+ options[:hidpi] = true
135
+ end
136
+
137
+ transformations.reject(&:blank?).join("/")
138
+ end
139
+
140
+ def self.api_string_to_sign(params_to_sign)
141
+ params_to_sign.map{|k,v| [k.to_s, v.is_a?(Array) ? v.join(",") : v]}.reject{|k,v| v.nil? || v == ""}.sort_by(&:first).map{|k,v| "#{k}=#{v}"}.join("&")
142
+ end
143
+
144
+ def self.api_sign_request(params_to_sign, api_secret)
145
+ to_sign = api_string_to_sign(params_to_sign)
146
+ Digest::SHA1.hexdigest("#{to_sign}#{api_secret}")
147
+ end
148
+
149
+ # Warning: options are being destructively updated!
150
+ def self.unsigned_download_url(source, options = {})
151
+
152
+ type = options.delete(:type)
153
+
154
+ options[:fetch_format] ||= options.delete(:format) if type == :fetch
155
+ transformation = self.generate_transformation_string(options)
156
+
157
+ resource_type = options.delete(:resource_type)
158
+ version = options.delete(:version)
159
+ format = options.delete(:format)
160
+ cloud_name = config_option_consume(options, :cloud_name) || raise(CloudinaryException, "Must supply cloud_name in tag or in configuration")
161
+
162
+ secure = options.delete(:secure)
163
+ ssl_detected = options.delete(:ssl_detected)
164
+ secure = ssl_detected || Cloudinary.config.secure if secure.nil?
165
+ private_cdn = config_option_consume(options, :private_cdn)
166
+ secure_distribution = config_option_consume(options, :secure_distribution)
167
+ cname = config_option_consume(options, :cname)
168
+ shorten = config_option_consume(options, :shorten)
169
+ force_remote = options.delete(:force_remote)
170
+ cdn_subdomain = config_option_consume(options, :cdn_subdomain)
171
+ secure_cdn_subdomain = config_option_consume(options, :secure_cdn_subdomain)
172
+ sign_url = config_option_consume(options, :sign_url)
173
+ secret = config_option_consume(options, :api_secret)
174
+ sign_version = config_option_consume(options, :sign_version) # Deprecated behavior
175
+ url_suffix = options.delete(:url_suffix)
176
+ use_root_path = config_option_consume(options, :use_root_path)
177
+
178
+ raise(CloudinaryException, "URL Suffix only supported in private CDN") if url_suffix.present? and not private_cdn
179
+
180
+ original_source = source
181
+ return original_source if source.blank?
182
+ if defined?(CarrierWave::Uploader::Base) && source.is_a?(CarrierWave::Uploader::Base)
183
+ resource_type ||= source.resource_type
184
+ type ||= source.storage_type
185
+ source = format.blank? ? source.filename : source.full_public_id
186
+ end
187
+ resource_type ||= "image"
188
+ source = source.to_s
189
+ if !force_remote
190
+ return original_source if (type.nil? || type == :asset) && source.match(%r(^https?:/)i)
191
+ if source.start_with?("/")
192
+ if source.start_with?("/images/")
193
+ source = source.sub(%r(/images/), '')
194
+ else
195
+ return original_source
196
+ end
197
+ end
198
+ @metadata ||= defined?(Cloudinary::Static) ? Cloudinary::Static.metadata : {}
199
+ if type == :asset && @metadata["images/#{source}"]
200
+ return original_source if !Cloudinary.config.static_image_support
201
+ source = @metadata["images/#{source}"]["public_id"]
202
+ source += File.extname(original_source) if !format
203
+ elsif type == :asset
204
+ return original_source # requested asset, but no metadata - probably local file. return.
205
+ end
206
+ end
207
+
208
+ resource_type, type = finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
209
+ source, source_to_sign = finalize_source(source, format, url_suffix)
210
+
211
+ version ||= 1 if source_to_sign.include?("/") and !source_to_sign.match(/^v[0-9]+/) and !source_to_sign.match(/^https?:\//)
212
+ version &&= "v#{version}"
213
+
214
+ transformation = transformation.gsub(%r(([^:])//), '\1/')
215
+ if sign_url
216
+ to_sign = [transformation, sign_version && version, source_to_sign].reject(&:blank?).join("/")
217
+ signature = 's--' + Base64.urlsafe_encode64(Digest::SHA1.digest(to_sign + secret))[0,8] + '--'
218
+ end
219
+
220
+ prefix = unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
221
+ source = [prefix, resource_type, type, signature, transformation, version, source].reject(&:blank?).join("/")
222
+ end
223
+
224
+ def self.finalize_source(source, format, url_suffix)
225
+ source = source.gsub(%r(([^:])//), '\1/')
226
+ if source.match(%r(^https?:/)i)
227
+ source = smart_escape(source)
228
+ source_to_sign = source
229
+ else
230
+ source = smart_escape(URI.decode(source))
231
+ source_to_sign = source
232
+ unless url_suffix.blank?
233
+ raise(CloudinaryException, "url_suffix should not include . or /") if url_suffix.match(%r([\./]))
234
+ source = "#{source}/#{url_suffix}"
235
+ end
236
+ if !format.blank?
237
+ source = "#{source}.#{format}"
238
+ source_to_sign = "#{source_to_sign}.#{format}"
239
+ end
240
+ end
241
+ [source, source_to_sign]
242
+ end
243
+
244
+ def self.finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
245
+ type ||= :upload
246
+ if !url_suffix.blank?
247
+ if resource_type.to_s == "image" && type.to_s == "upload"
248
+ resource_type = "images"
249
+ type = nil
250
+ elsif resource_type.to_s == "raw" && type.to_s == "upload"
251
+ resource_type = "files"
252
+ type = nil
253
+ else
254
+ raise(CloudinaryException, "URL Suffix only supported for image/upload and raw/upload")
255
+ end
256
+ end
257
+ if use_root_path
258
+ if (resource_type.to_s == "image" && type.to_s == "upload") || (resource_type.to_s == "images" && type.blank?)
259
+ resource_type = nil
260
+ type = nil
261
+ else
262
+ raise(CloudinaryException, "Root path only supported for image/upload")
263
+ end
264
+ end
265
+ if shorten && resource_type.to_s == "image" && type.to_s == "upload"
266
+ resource_type = "iu"
267
+ type = nil
268
+ end
269
+ [resource_type, type]
270
+ end
271
+
272
+ # cdn_subdomain and secure_cdn_subdomain
273
+ # 1) Customers in shared distribution (e.g. res.cloudinary.com)
274
+ # if cdn_domain is true uses res-[1-5].cloudinary.com for both http and https. Setting secure_cdn_subdomain to false disables this for https.
275
+ # 2) Customers with private cdn
276
+ # if cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for http
277
+ # if secure_cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for https (please contact support if you require this)
278
+ # 3) Customers with cname
279
+ # if cdn_domain is true uses a[1-5].cname for http. For https, uses the same naming scheme as 1 for shared distribution and as 2 for private distribution.
280
+ def self.unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
281
+ return "/res#{cloud_name}" if cloud_name.start_with?("/") # For development
282
+
283
+ shared_domain = !private_cdn
284
+
285
+ if secure
286
+ if secure_distribution.nil? || secure_distribution == Cloudinary::OLD_AKAMAI_SHARED_CDN
287
+ secure_distribution = private_cdn ? "#{cloud_name}-res.cloudinary.com" : Cloudinary::SHARED_CDN
288
+ end
289
+ shared_domain ||= secure_distribution == Cloudinary::SHARED_CDN
290
+ secure_cdn_subdomain = cdn_subdomain if secure_cdn_subdomain.nil? && shared_domain
291
+
292
+ if secure_cdn_subdomain
293
+ secure_distribution = secure_distribution.gsub('res.cloudinary.com', "res-#{(Zlib::crc32(source) % 5) + 1}.cloudinary.com")
294
+ end
295
+
296
+ prefix = "https://#{secure_distribution}"
297
+ elsif cname
298
+ subdomain = cdn_subdomain ? "a#{(Zlib::crc32(source) % 5) + 1}." : ""
299
+ prefix = "http://#{subdomain}#{cname}"
300
+ else
301
+ host = [private_cdn ? "#{cloud_name}-" : "", "res", cdn_subdomain ? "-#{(Zlib::crc32(source) % 5) + 1}" : "", ".cloudinary.com"].join
302
+ prefix = "http://#{host}"
303
+ end
304
+ prefix += "/#{cloud_name}" if shared_domain
305
+
306
+ prefix
307
+ end
308
+
309
+ def self.cloudinary_api_url(action = 'upload', options = {})
310
+ cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || "https://api.cloudinary.com"
311
+ cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise(CloudinaryException, "Must supply cloud_name")
312
+ resource_type = options[:resource_type] || "image"
313
+ return [cloudinary, "v1_1", cloud_name, resource_type, action].join("/")
314
+ end
315
+
316
+ def self.sign_request(params, options={})
317
+ api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
318
+ api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
319
+ params = params.reject{|k, v| self.safe_blank?(v)}
320
+ params[:signature] = Cloudinary::Utils.api_sign_request(params, api_secret)
321
+ params[:api_key] = api_key
322
+ params
323
+ end
324
+
325
+ def self.private_download_url(public_id, format, options = {})
326
+ cloudinary_params = sign_request({
327
+ :timestamp=>Time.now.to_i,
328
+ :public_id=>public_id,
329
+ :format=>format,
330
+ :type=>options[:type],
331
+ :attachment=>options[:attachment],
332
+ :expires_at=>options[:expires_at] && options[:expires_at].to_i
333
+ }, options)
334
+
335
+ return Cloudinary::Utils.cloudinary_api_url("download", options) + "?" + cloudinary_params.to_query
336
+ end
337
+
338
+ def self.zip_download_url(tag, options = {})
339
+ cloudinary_params = sign_request({:timestamp=>Time.now.to_i, :tag=>tag, :transformation=>generate_transformation_string(options)}, options)
340
+ return Cloudinary::Utils.cloudinary_api_url("download_tag.zip", options) + "?" + cloudinary_params.to_query
341
+ end
342
+
343
+ def self.signed_download_url(public_id, options = {})
344
+ aws_private_key_path = options[:aws_private_key_path] || Cloudinary.config.aws_private_key_path || raise(CloudinaryException, "Must supply aws_private_key_path")
345
+ aws_key_pair_id = options[:aws_key_pair_id] || Cloudinary.config.aws_key_pair_id || raise(CloudinaryException, "Must supply aws_key_pair_id")
346
+ authenticated_distribution = options[:authenticated_distribution] || Cloudinary.config.authenticated_distribution || raise(CloudinaryException, "Must supply authenticated_distribution")
347
+ @signers ||= Hash.new{|h,k| path, id = k; h[k] = AwsCfSigner.new(path, id)}
348
+ signer = @signers[[aws_private_key_path, aws_key_pair_id]]
349
+ url = Cloudinary::Utils.unsigned_download_url(public_id, {:type=>:authenticated}.merge(options).merge(:secure=>true, :secure_distribution=>authenticated_distribution, :private_cdn=>true))
350
+ expires_at = options[:expires_at] || (Time.now+3600)
351
+ signer.sign(url, :ending => expires_at)
352
+ end
353
+
354
+ def self.cloudinary_url(public_id, options = {})
355
+ if options[:type].to_s == 'authenticated' && !options[:sign_url]
356
+ result = signed_download_url(public_id, options)
357
+ else
358
+ result = unsigned_download_url(public_id, options)
359
+ end
360
+ return result
361
+ end
362
+
363
+ def self.asset_file_name(path)
364
+ data = Cloudinary.app_root.join(path).read(:mode=>"rb")
365
+ ext = path.extname
366
+ md5 = Digest::MD5.hexdigest(data)
367
+ public_id = "#{path.basename(ext)}-#{md5}"
368
+ "#{public_id}#{ext}"
369
+ end
370
+
371
+ # Based on CGI::unescape. In addition does not escape / :
372
+ def self.smart_escape(string)
373
+ string.gsub(/([^a-zA-Z0-9_.\-\/:]+)/) do
374
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
375
+ end
376
+ end
377
+
378
+ def self.random_public_id
379
+ sr = defined?(ActiveSupport::SecureRandom) ? ActiveSupport::SecureRandom : SecureRandom
380
+ sr.base64(20).downcase.gsub(/[^a-z0-9]/, "").sub(/^[0-9]+/, '')[0,20]
381
+ end
382
+
383
+ def self.signed_preloaded_image(result)
384
+ "#{result["resource_type"]}/#{result["type"] || "upload"}/v#{result["version"]}/#{[result["public_id"], result["format"]].reject(&:blank?).join(".")}##{result["signature"]}"
385
+ end
386
+
387
+ @@json_decode = false
388
+ def self.json_decode(str)
389
+ if !@@json_decode
390
+ @@json_decode = true
391
+ begin
392
+ require 'json'
393
+ rescue LoadError
394
+ begin
395
+ require 'active_support/json'
396
+ rescue LoadError
397
+ raise LoadError, "Please add the json gem or active_support to your Gemfile"
398
+ end
399
+ end
400
+ end
401
+ defined?(JSON) ? JSON.parse(str) : ActiveSupport::JSON.decode(str)
402
+ end
403
+
404
+ def self.build_array(array)
405
+ case array
406
+ when Array then array
407
+ when nil then []
408
+ else [array]
409
+ end
410
+ end
411
+
412
+ def self.encode_hash(hash)
413
+ case hash
414
+ when Hash then hash.map{|k,v| "#{k}=#{v}"}.join("|")
415
+ when nil then ""
416
+ else hash
417
+ end
418
+ end
419
+
420
+ def self.encode_double_array(array)
421
+ array = build_array(array)
422
+ if array.length > 0 && array[0].is_a?(Array)
423
+ return array.map{|a| build_array(a).join(",")}.join("|")
424
+ else
425
+ return array.join(",")
426
+ end
427
+ end
428
+
429
+ IMAGE_FORMATS = %w(bmp png tif tiff jpg jpeg gif pdf ico eps jpc jp2 psd)
430
+
431
+ def self.supported_image_format?(format)
432
+ format = format.to_s.downcase
433
+ extension = format =~ /\./ ? format.split('.').last : format
434
+ IMAGE_FORMATS.include?(extension)
435
+ end
436
+
437
+ def self.resource_type_for_format(format)
438
+ self.supported_image_format?(format) ? 'image' : 'raw'
439
+ end
440
+
441
+ def self.config_option_consume(options, option_name, default_value = nil)
442
+ return options.delete(option_name) if options.include?(option_name)
443
+ return Cloudinary.config.send(option_name) || default_value
444
+ end
445
+
446
+ def self.as_bool(value)
447
+ case value
448
+ when nil then nil
449
+ when String then value.downcase == "true" || value == "1"
450
+ when TrueClass then true
451
+ when FalseClass then false
452
+ when Fixnum then value != 0
453
+ when Symbol then value == :true
454
+ else
455
+ raise "Invalid boolean value #{value} of type #{value.class}"
456
+ end
457
+ end
458
+
459
+ def self.as_safe_bool(value)
460
+ case as_bool(value)
461
+ when nil then nil
462
+ when TrueClass then 1
463
+ when FalseClass then 0
464
+ end
465
+ end
466
+
467
+ def self.safe_blank?(value)
468
+ value.nil? || value == "" || value == []
469
+ end
470
+
471
+ private
472
+ def self.number_pattern
473
+ "([0-9]*)\\.([0-9]+)|([0-9]+)"
474
+ end
475
+
476
+ def self.offset_any_pattern
477
+ "(#{number_pattern})([%pP])?"
478
+ end
479
+
480
+ def self.offset_any_pattern_re
481
+ /((([0-9]*)\.([0-9]+)|([0-9]+))([%pP])?)\.\.((([0-9]*)\.([0-9]+)|([0-9]+))([%pP])?)/
482
+ end
483
+
484
+ # Split a range into the start and end values
485
+ def self.split_range(range) # :nodoc:
486
+ case range
487
+ when Range
488
+ [range.first, range.last]
489
+ when String
490
+ range.split ".." if offset_any_pattern_re =~ range
491
+ when Array
492
+ [range.first, range.last]
493
+ else
494
+ nil
495
+ end
496
+ end
497
+
498
+ # Normalize an offset value
499
+ # @param [String] value a decimal value which may have a 'p' or '%' postfix. E.g. '35%', '0.4p'
500
+ # @return [Object|String] a normalized String of the input value if possible otherwise the value itself
501
+ def self.norm_range_value(value) # :nodoc:
502
+ offset = /^#{offset_any_pattern}$/.match( value.to_s)
503
+ if offset
504
+ modifier = offset[5].present? ? 'p' : ''
505
+ value = "#{offset[1]}#{modifier}"
506
+ end
507
+ value
508
+ end
509
+
510
+ # A video codec parameter can be either a String or a Hash.
511
+ #
512
+ # @param [Object] param <code>vc_<codec>[ : <profile> : [<level>]]</code>
513
+ # or <code>{ codec: 'h264', profile: 'basic', level: '3.1' }</code>
514
+ # @return [String] <code><codec> : <profile> : [<level>]]</code> if a Hash was provided
515
+ # or the param if a String was provided.
516
+ # Returns NIL if param is not a Hash or String
517
+ def self.process_video_params(param)
518
+ case param
519
+ when Hash
520
+ video = ""
521
+ if param.has_key? :codec
522
+ video = param[:codec]
523
+ if param.has_key? :profile
524
+ video.concat ":" + param[:profile]
525
+ if param.has_key? :level
526
+ video.concat ":" + param[:level]
527
+ end
528
+ end
529
+ end
530
+ video
531
+ when String
532
+ param
533
+ else
534
+ nil
535
+ end
536
+ end
537
+
538
+ def self.deep_symbolize_keys(object)
539
+ case object
540
+ when Hash
541
+ result = {}
542
+ object.each do |key, value|
543
+ key = key.to_sym rescue key
544
+ result[key] = deep_symbolize_keys(value)
545
+ end
546
+ result
547
+ when Array
548
+ object.map{|e| deep_symbolize_keys(e)}
549
+ else
550
+ object
551
+ end
552
+ end
553
+
554
+ end