cloudinary 1.9.1 → 1.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +42 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
  4. data/.github/pull_request_template.md +24 -0
  5. data/.gitignore +7 -1
  6. data/.travis.yml +15 -8
  7. data/CHANGELOG.md +261 -0
  8. data/README.md +3 -0
  9. data/Rakefile +3 -45
  10. data/cloudinary.gemspec +27 -20
  11. data/lib/active_storage/blob_key.rb +20 -0
  12. data/lib/active_storage/service/cloudinary_service.rb +249 -0
  13. data/lib/cloudinary.rb +53 -63
  14. data/lib/cloudinary/account_api.rb +231 -0
  15. data/lib/cloudinary/account_config.rb +30 -0
  16. data/lib/cloudinary/api.rb +228 -71
  17. data/lib/cloudinary/auth_token.rb +10 -4
  18. data/lib/cloudinary/base_api.rb +79 -0
  19. data/lib/cloudinary/base_config.rb +70 -0
  20. data/lib/cloudinary/cache.rb +38 -0
  21. data/lib/cloudinary/cache/breakpoints_cache.rb +31 -0
  22. data/lib/cloudinary/cache/key_value_cache_adapter.rb +25 -0
  23. data/lib/cloudinary/cache/rails_cache_adapter.rb +34 -0
  24. data/lib/cloudinary/cache/storage/rails_cache_storage.rb +5 -0
  25. data/lib/cloudinary/carrier_wave.rb +4 -2
  26. data/lib/cloudinary/carrier_wave/remote.rb +3 -2
  27. data/lib/cloudinary/carrier_wave/storage.rb +2 -1
  28. data/lib/cloudinary/{controller.rb → cloudinary_controller.rb} +3 -5
  29. data/lib/cloudinary/config.rb +43 -0
  30. data/lib/cloudinary/helper.rb +77 -7
  31. data/lib/cloudinary/migrator.rb +3 -1
  32. data/lib/cloudinary/railtie.rb +7 -3
  33. data/lib/cloudinary/responsive.rb +111 -0
  34. data/lib/cloudinary/uploader.rb +67 -15
  35. data/lib/cloudinary/utils.rb +324 -54
  36. data/lib/cloudinary/version.rb +1 -1
  37. data/lib/cloudinary/video_helper.rb +96 -22
  38. data/lib/tasks/cloudinary/fetch_assets.rake +48 -0
  39. data/lib/tasks/{cloudinary.rake → cloudinary/sync_static.rake} +0 -0
  40. data/tools/allocate_test_cloud.sh +9 -0
  41. data/tools/get_test_cloud.sh +9 -0
  42. data/tools/update_version +220 -0
  43. data/vendor/assets/javascripts/cloudinary/jquery.cloudinary.js +51 -13
  44. data/vendor/assets/javascripts/cloudinary/jquery.fileupload.js +24 -4
  45. data/vendor/assets/javascripts/cloudinary/jquery.ui.widget.js +741 -561
  46. data/vendor/assets/javascripts/cloudinary/load-image.all.min.js +1 -1
  47. metadata +92 -67
  48. data/spec/access_control_spec.rb +0 -99
  49. data/spec/api_spec.rb +0 -545
  50. data/spec/archive_spec.rb +0 -129
  51. data/spec/auth_token_spec.rb +0 -79
  52. data/spec/cloudinary_helper_spec.rb +0 -190
  53. data/spec/cloudinary_spec.rb +0 -32
  54. data/spec/data/sync_static/app/assets/javascripts/1.coffee +0 -1
  55. data/spec/data/sync_static/app/assets/javascripts/1.js +0 -1
  56. data/spec/data/sync_static/app/assets/stylesheets/1.css +0 -3
  57. data/spec/docx.docx +0 -0
  58. data/spec/favicon.ico +0 -0
  59. data/spec/logo.png +0 -0
  60. data/spec/rake_spec.rb +0 -160
  61. data/spec/sample_asset_file.tsv +0 -4
  62. data/spec/search_spec.rb +0 -109
  63. data/spec/spec_helper.rb +0 -245
  64. data/spec/storage_spec.rb +0 -44
  65. data/spec/streaminig_profiles_api_spec.rb +0 -74
  66. data/spec/support/helpers/temp_file_helpers.rb +0 -22
  67. data/spec/support/shared_contexts/rake.rb +0 -19
  68. data/spec/uploader_spec.rb +0 -363
  69. data/spec/utils_methods_spec.rb +0 -54
  70. data/spec/utils_spec.rb +0 -906
  71. data/spec/video_tag_spec.rb +0 -251
  72. data/spec/video_url_spec.rb +0 -164
@@ -1,4 +1,6 @@
1
1
  # Copyright Cloudinary
2
+
3
+ # frozen_string_literal: true
2
4
  require 'digest/sha1'
3
5
  require 'zlib'
4
6
  require 'uri'
@@ -6,6 +8,7 @@ require 'aws_cf_signer'
6
8
  require 'json'
7
9
  require 'cgi'
8
10
  require 'cloudinary/auth_token'
11
+ require 'cloudinary/responsive'
9
12
 
10
13
  class Cloudinary::Utils
11
14
  # @deprecated Use Cloudinary::SHARED_CDN
@@ -23,30 +26,172 @@ class Cloudinary::Utils
23
26
  "*" => 'mul',
24
27
  "/" => 'div',
25
28
  "+" => 'add',
26
- "-" => 'sub'
29
+ "-" => 'sub',
30
+ "^" => 'pow'
27
31
  }
28
32
 
29
33
  PREDEFINED_VARS = {
30
34
  "aspect_ratio" => "ar",
35
+ "aspectRatio" => "ar",
31
36
  "current_page" => "cp",
37
+ "currentPage" => "cp",
32
38
  "face_count" => "fc",
39
+ "faceCount" => "fc",
33
40
  "height" => "h",
34
41
  "initial_aspect_ratio" => "iar",
42
+ "initialAspectRatio" => "iar",
43
+ "trimmed_aspect_ratio" => "tar",
44
+ "trimmedAspectRatio" => "tar",
35
45
  "initial_height" => "ih",
46
+ "initialHeight" => "ih",
36
47
  "initial_width" => "iw",
48
+ "initialWidth" => "iw",
37
49
  "page_count" => "pc",
50
+ "pageCount" => "pc",
38
51
  "page_x" => "px",
52
+ "pageX" => "px",
39
53
  "page_y" => "py",
54
+ "pageY" => "py",
40
55
  "tags" => "tags",
41
- "width" => "w"
56
+ "initial_duration" => "idu",
57
+ "initialDuration" => "idu",
58
+ "duration" => "du",
59
+ "width" => "w",
60
+ "illustration_score" => "ils",
61
+ "illustrationScore" => "ils",
62
+ "context" => "ctx"
63
+ }
64
+
65
+ SIMPLE_TRANSFORMATION_PARAMS = {
66
+ :ac => :audio_codec,
67
+ :af => :audio_frequency,
68
+ :br => :bit_rate,
69
+ :cs => :color_space,
70
+ :d => :default_image,
71
+ :dl => :delay,
72
+ :dn => :density,
73
+ :du => :duration,
74
+ :eo => :end_offset,
75
+ :f => :fetch_format,
76
+ :g => :gravity,
77
+ :ki => :keyframe_interval,
78
+ :p => :prefix,
79
+ :pg => :page,
80
+ :so => :start_offset,
81
+ :sp => :streaming_profile,
82
+ :vc => :video_codec,
83
+ :vs => :video_sampling
84
+ }.freeze
85
+
86
+ URL_KEYS = %w[
87
+ api_secret
88
+ auth_token
89
+ cdn_subdomain
90
+ cloud_name
91
+ cname
92
+ format
93
+ private_cdn
94
+ resource_type
95
+ secure
96
+ secure_cdn_subdomain
97
+ secure_distribution
98
+ shorten
99
+ sign_url
100
+ ssl_detected
101
+ type
102
+ url_suffix
103
+ use_root_path
104
+ version
105
+ ].map(&:to_sym)
106
+
107
+
108
+ TRANSFORMATION_PARAMS = %w[
109
+ angle
110
+ aspect_ratio
111
+ audio_codec
112
+ audio_frequency
113
+ background
114
+ bit_rate
115
+ border
116
+ color
117
+ color_space
118
+ crop
119
+ custom_function
120
+ default_image
121
+ delay
122
+ density
123
+ dpr
124
+ duration
125
+ effect
126
+ end_offset
127
+ fetch_format
128
+ flags
129
+ fps
130
+ gravity
131
+ height
132
+ if
133
+ keyframe_interval
134
+ offset
135
+ opacity
136
+ overlay
137
+ page
138
+ prefix
139
+ quality
140
+ radius
141
+ raw_transformation
142
+ responsive_width
143
+ size
144
+ start_offset
145
+ streaming_profile
146
+ transformation
147
+ underlay
148
+ variables
149
+ video_codec
150
+ video_sampling
151
+ width
152
+ x
153
+ y
154
+ zoom
155
+ ].map(&:to_sym)
156
+
157
+ REMOTE_URL_REGEX = %r(^ftp:|^https?:|^s3:|^gs:|^data:([\w-]+\/[\w-]+(\+[\w-]+)?)?(;[\w-]+=[\w-]+)*;base64,([a-zA-Z0-9\/+\n=]+)$)
158
+
159
+ LONG_URL_SIGNATURE_LENGTH = 32
160
+ SHORT_URL_SIGNATURE_LENGTH = 8
161
+
162
+ UPLOAD_PREFIX = 'https://api.cloudinary.com'
163
+
164
+ ALGO_SHA1 = :sha1
165
+ ALGO_SHA256 = :sha256
166
+
167
+ ALGORITHM_SIGNATURE = {
168
+ ALGO_SHA1 => Digest::SHA1,
169
+ ALGO_SHA256 => Digest::SHA256,
42
170
  }
171
+
172
+ def self.extract_config_params(options)
173
+ options.select{|k,v| URL_KEYS.include?(k)}
174
+ end
175
+
176
+ def self.extract_transformation_params(options)
177
+ options.select{|k,v| TRANSFORMATION_PARAMS.include?(k)}
178
+ end
179
+
180
+ def self.chain_transformation(options, *transformation)
181
+ base_options = extract_config_params(options)
182
+ transformation = transformation.reject(&:nil?)
183
+ base_options[:transformation] = build_array(extract_transformation_params(options)).concat(transformation)
184
+ base_options
185
+ end
186
+
187
+
43
188
  # Warning: options are being destructively updated!
44
189
  def self.generate_transformation_string(options={}, allow_implicit_crop_mode = false)
45
190
  # allow_implicit_crop_mode was added to support height and width parameters without specifying a crop mode.
46
191
  # This only apply to this (cloudinary_gem) SDK
47
192
 
48
193
  if options.is_a?(Array)
49
- return options.map{|base_transformation| generate_transformation_string(base_transformation.clone, allow_implicit_crop_mode)}.join("/")
194
+ return options.map{|base_transformation| generate_transformation_string(base_transformation.clone, allow_implicit_crop_mode)}.reject(&:blank?).join("/")
50
195
  end
51
196
 
52
197
  symbolize_keys!(options)
@@ -102,9 +247,14 @@ class Cloudinary::Utils
102
247
  options[:start_offset], options[:end_offset] = split_range options.delete(:offset)
103
248
  end
104
249
 
250
+ fps = options.delete(:fps)
251
+ fps = fps.join('-') if fps.is_a? Array
252
+
105
253
  overlay = process_layer(options.delete(:overlay))
106
254
  underlay = process_layer(options.delete(:underlay))
107
255
  ifValue = process_if(options.delete(:if))
256
+ custom_function = process_custom_function(options.delete(:custom_function))
257
+ custom_pre_function = process_custom_pre_function(options.delete(:custom_pre_function))
108
258
 
109
259
  params = {
110
260
  :a => normalize_expression(angle),
@@ -116,11 +266,13 @@ class Cloudinary::Utils
116
266
  :dpr => normalize_expression(dpr),
117
267
  :e => normalize_expression(effect),
118
268
  :fl => flags,
269
+ :fn => custom_function || custom_pre_function,
270
+ :fps => fps,
119
271
  :h => normalize_expression(height),
120
272
  :l => overlay,
121
273
  :o => normalize_expression(options.delete(:opacity)),
122
274
  :q => normalize_expression(options.delete(:quality)),
123
- :r => normalize_expression(options.delete(:radius)),
275
+ :r => process_radius(options.delete(:radius)),
124
276
  :t => named_transformation,
125
277
  :u => underlay,
126
278
  :w => normalize_expression(width),
@@ -128,26 +280,7 @@ class Cloudinary::Utils
128
280
  :y => normalize_expression(options.delete(:y)),
129
281
  :z => normalize_expression(options.delete(:zoom))
130
282
  }
131
- {
132
- :ac => :audio_codec,
133
- :af => :audio_frequency,
134
- :br => :bit_rate,
135
- :cs => :color_space,
136
- :d => :default_image,
137
- :dl => :delay,
138
- :dn => :density,
139
- :du => :duration,
140
- :eo => :end_offset,
141
- :f => :fetch_format,
142
- :g => :gravity,
143
- :ki => :keyframe_interval,
144
- :p => :prefix,
145
- :pg => :page,
146
- :so => :start_offset,
147
- :sp => :streaming_profile,
148
- :vc => :video_codec,
149
- :vs => :video_sampling
150
- }.each do
283
+ SIMPLE_TRANSFORMATION_PARAMS.each do
151
284
  |param, option|
152
285
  params[param] = options.delete(option)
153
286
  end
@@ -197,22 +330,20 @@ class Cloudinary::Utils
197
330
  # Translates the condition if provided.
198
331
  # @return [string] "if_" + ifValue
199
332
  # @private
200
- def self.process_if(ifValue)
201
- if ifValue
202
- ifValue = normalize_expression(ifValue)
203
-
204
- ifValue = "if_" + ifValue
205
- end
333
+ def self.process_if(if_value)
334
+ "if_" + normalize_expression(if_value) unless if_value.to_s.empty?
206
335
  end
207
336
 
208
- EXP_REGEXP = Regexp.new(PREDEFINED_VARS.keys.join("|")+'|('+CONDITIONAL_OPERATORS.keys.reverse.map { |k| Regexp.escape(k) }.join('|')+')(?=[ _])')
337
+ EXP_REGEXP = Regexp.new('(\$_*[^_ ]+)|(?<!\$)('+PREDEFINED_VARS.keys.join("|")+')'+'|('+CONDITIONAL_OPERATORS.keys.reverse.map { |k| Regexp.escape(k) }.join('|')+')(?=[ _])')
209
338
  EXP_REPLACEMENT = PREDEFINED_VARS.merge(CONDITIONAL_OPERATORS)
210
339
 
211
340
  def self.normalize_expression(expression)
212
- if expression =~ /^!.+!$/ # quoted string
341
+ if expression.nil?
342
+ nil
343
+ elsif expression.is_a?( String) && expression =~ /^!.+!$/ # quoted string
213
344
  expression
214
345
  else
215
- expression.to_s.gsub(EXP_REGEXP,EXP_REPLACEMENT).gsub(/[ _]+/, "_")
346
+ expression.to_s.gsub(EXP_REGEXP) { |match| EXP_REPLACEMENT[match] || match }.gsub(/[ _]+/, "_")
216
347
  end
217
348
  end
218
349
 
@@ -230,9 +361,13 @@ class Cloudinary::Utils
230
361
  text_style = nil
231
362
  components = []
232
363
 
233
- unless public_id.blank?
234
- public_id = public_id.gsub("/", ":")
235
- public_id = "#{public_id}.#{format}" unless format.nil?
364
+ if public_id.present?
365
+ if type == "fetch" && public_id.match(%r(^https?:/)i)
366
+ public_id = Base64.urlsafe_encode64(public_id)
367
+ else
368
+ public_id = public_id.gsub("/", ":")
369
+ public_id = "#{public_id}.#{format}" if format
370
+ end
236
371
  end
237
372
 
238
373
  if text.blank? && resource_type != "text"
@@ -275,6 +410,17 @@ class Cloudinary::Utils
275
410
  end
276
411
  private_class_method :process_layer
277
412
 
413
+ # Parse radius options
414
+ # @return [string] radius transformation string
415
+ # @private
416
+ def self.process_radius(radius)
417
+ if radius.is_a?(Array) && !radius.length.between?(1, 4)
418
+ raise(CloudinaryException, "Invalid radius parameter")
419
+ end
420
+ Array(radius).map { |r| normalize_expression(r) }.join(":")
421
+ end
422
+ private_class_method :process_radius
423
+
278
424
  LAYER_KEYWORD_PARAMS =[
279
425
  [:font_weight ,"normal"],
280
426
  [:font_style ,"normal"],
@@ -295,6 +441,10 @@ class Cloudinary::Utils
295
441
  keywords.push("letter_spacing_#{letter_spacing}") unless letter_spacing.blank?
296
442
  line_spacing = layer[:line_spacing]
297
443
  keywords.push("line_spacing_#{line_spacing}") unless line_spacing.blank?
444
+ font_antialiasing = layer[:font_antialiasing]
445
+ keywords.push("antialias_#{font_antialiasing}") unless font_antialiasing.blank?
446
+ font_hinting = layer[:font_hinting]
447
+ keywords.push("hinting_#{font_hinting}") unless font_hinting.blank?
298
448
  if !font_size.blank? || !font_family.blank? || !keywords.empty?
299
449
  raise(CloudinaryException, "Must supply font_family for text in overlay/underlay") if font_family.blank?
300
450
  raise(CloudinaryException, "Must supply font_size for text in overlay/underlay") if font_size.blank?
@@ -308,9 +458,9 @@ class Cloudinary::Utils
308
458
  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("&")
309
459
  end
310
460
 
311
- def self.api_sign_request(params_to_sign, api_secret)
461
+ def self.api_sign_request(params_to_sign, api_secret, signature_algorithm = nil)
312
462
  to_sign = api_string_to_sign(params_to_sign)
313
- Digest::SHA1.hexdigest("#{to_sign}#{api_secret}")
463
+ hash("#{to_sign}#{api_secret}", signature_algorithm, :hexdigest)
314
464
  end
315
465
 
316
466
  # Returns a JSON array as String.
@@ -348,13 +498,14 @@ class Cloudinary::Utils
348
498
  # Warning: options are being destructively updated!
349
499
  def self.unsigned_download_url(source, options = {})
350
500
 
501
+ patch_fetch_format(options)
351
502
  type = options.delete(:type)
352
503
 
353
- options[:fetch_format] ||= options.delete(:format) if type.to_s == "fetch"
354
504
  transformation = self.generate_transformation_string(options)
355
505
 
356
506
  resource_type = options.delete(:resource_type)
357
507
  version = options.delete(:version)
508
+ force_version = config_option_consume(options, :force_version, true)
358
509
  format = options.delete(:format)
359
510
  cloud_name = config_option_consume(options, :cloud_name) || raise(CloudinaryException, "Must supply cloud_name in tag or in configuration")
360
511
 
@@ -374,6 +525,8 @@ class Cloudinary::Utils
374
525
  url_suffix = options.delete(:url_suffix)
375
526
  use_root_path = config_option_consume(options, :use_root_path)
376
527
  auth_token = config_option_consume(options, :auth_token)
528
+ long_url_signature = config_option_consume(options, :long_url_signature)
529
+ signature_algorithm = config_option_consume(options, :signature_algorithm)
377
530
  unless auth_token == false
378
531
  auth_token = Cloudinary::AuthToken.merge_auth_token(Cloudinary.config.auth_token, auth_token)
379
532
  end
@@ -405,14 +558,23 @@ class Cloudinary::Utils
405
558
  resource_type, type = finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten)
406
559
  source, source_to_sign = finalize_source(source, format, url_suffix)
407
560
 
408
- version ||= 1 if source_to_sign.include?("/") and !source_to_sign.match(/^v[0-9]+/) and !source_to_sign.match(/^https?:\//)
561
+ if version.nil? && force_version &&
562
+ source_to_sign.include?("/") &&
563
+ !source_to_sign.match(/^v[0-9]+/) &&
564
+ !source_to_sign.match(/^https?:\//)
565
+ version = 1
566
+ end
409
567
  version &&= "v#{version}"
410
568
 
411
569
  transformation = transformation.gsub(%r(([^:])//), '\1/')
412
570
  if sign_url && ( !auth_token || auth_token.empty?)
571
+ raise(CloudinaryException, "Must supply api_secret") if (secret.nil? || secret.empty?)
413
572
  to_sign = [transformation, sign_version && version, source_to_sign].reject(&:blank?).join("/")
414
573
  to_sign = fully_unescape(to_sign)
415
- signature = 's--' + Base64.urlsafe_encode64(Digest::SHA1.digest(to_sign + secret))[0,8] + '--'
574
+ signature_algorithm = long_url_signature ? ALGO_SHA256 : signature_algorithm
575
+ hash = hash("#{to_sign}#{secret}", signature_algorithm)
576
+ signature = Base64.urlsafe_encode64(hash)
577
+ signature = "s--#{signature[0, long_url_signature ? LONG_URL_SIGNATURE_LENGTH : SHORT_URL_SIGNATURE_LENGTH ]}--"
416
578
  end
417
579
 
418
580
  prefix = unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
@@ -432,7 +594,7 @@ class Cloudinary::Utils
432
594
  source = smart_escape(source)
433
595
  source_to_sign = source
434
596
  else
435
- source = smart_escape(URI.decode(source))
597
+ source = smart_escape(smart_unescape(source))
436
598
  source_to_sign = source
437
599
  unless url_suffix.blank?
438
600
  raise(CloudinaryException, "url_suffix should not include . or /") if url_suffix.match(%r([\./]))
@@ -528,18 +690,31 @@ class Cloudinary::Utils
528
690
  prefix
529
691
  end
530
692
 
693
+ # Creates a base URL for the cloudinary api
694
+ #
695
+ # @param [Object] path Resource name
696
+ # @param [Hash] options Additional options
697
+ #
698
+ # @return [String]
699
+ def self.base_api_url(path, options = {})
700
+ cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || UPLOAD_PREFIX
701
+ cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise(CloudinaryException, 'Must supply cloud_name')
702
+
703
+ [cloudinary, 'v1_1', cloud_name, path].join('/')
704
+ end
705
+
531
706
  def self.cloudinary_api_url(action = 'upload', options = {})
532
- cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || "https://api.cloudinary.com"
533
- cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise(CloudinaryException, "Must supply cloud_name")
534
- resource_type = options[:resource_type] || "image"
535
- return [cloudinary, "v1_1", cloud_name, resource_type, action].join("/")
707
+ resource_type = options[:resource_type] || 'image'
708
+
709
+ base_api_url([resource_type, action], options)
536
710
  end
537
711
 
538
712
  def self.sign_request(params, options={})
539
713
  api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
540
714
  api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
715
+ signature_algorithm = options[:signature_algorithm]
541
716
  params = params.reject{|k, v| self.safe_blank?(v)}
542
- params[:signature] = Cloudinary::Utils.api_sign_request(params, api_secret)
717
+ params[:signature] = api_sign_request(params, api_secret, signature_algorithm)
543
718
  params[:api_key] = api_key
544
719
  params
545
720
  end
@@ -603,6 +778,18 @@ class Cloudinary::Utils
603
778
  download_archive_url(options.merge(:target_format => "zip"))
604
779
  end
605
780
 
781
+ # Creates and returns a URL that when invoked creates an archive of a folder.
782
+ #
783
+ # @param [Object] folder_path Full path (from the root) of the folder to download.
784
+ # @param [Hash] options Additional options.
785
+ #
786
+ # @return [String]
787
+ def self.download_folder(folder_path, options = {})
788
+ resource_type = options[:resource_type] || "all"
789
+
790
+ download_archive_url(options.merge(:resource_type => resource_type, :prefixes => folder_path))
791
+ end
792
+
606
793
  def self.signed_download_url(public_id, options = {})
607
794
  aws_private_key_path = options[:aws_private_key_path] || Cloudinary.config.aws_private_key_path
608
795
  if aws_private_key_path
@@ -636,13 +823,18 @@ class Cloudinary::Utils
636
823
  "#{public_id}#{ext}"
637
824
  end
638
825
 
639
- # Based on CGI::unescape. In addition does not escape / :
826
+ # Based on CGI::escape. In addition does not escape / :
640
827
  def self.smart_escape(string, unsafe = /([^a-zA-Z0-9_.\-\/:]+)/)
641
828
  string.gsub(unsafe) do |m|
642
829
  '%' + m.unpack('H2' * m.bytesize).join('%').upcase
643
830
  end
644
831
  end
645
832
 
833
+ # Based on CGI::unescape. In addition keeps '+' character as is
834
+ def self.smart_unescape(string)
835
+ CGI.unescape(string.sub('+', '%2B'))
836
+ end
837
+
646
838
  def self.random_public_id
647
839
  sr = defined?(ActiveSupport::SecureRandom) ? ActiveSupport::SecureRandom : SecureRandom
648
840
  sr.base64(20).downcase.gsub(/[^a-z0-9]/, "").sub(/^[0-9]+/, '')[0,20]
@@ -710,7 +902,7 @@ class Cloudinary::Utils
710
902
  end
711
903
  end
712
904
 
713
- IMAGE_FORMATS = %w(ai bmp bpg djvu eps eps3 flif gif hdp hpx ico j2k jp2 jpc jpe jpg miff pdf png psd svg tif tiff wdp webp zip )
905
+ IMAGE_FORMATS = %w(ai bmp bpg djvu eps eps3 flif gif hdp hpx ico j2k jp2 jpc jpe jpeg jpg miff pdf png psd svg tif tiff wdp webp zip )
714
906
 
715
907
  AUDIO_FORMATS = %w(aac aifc aiff flac m4a mp3 ogg wav)
716
908
 
@@ -730,10 +922,8 @@ class Cloudinary::Utils
730
922
  case
731
923
  when self.supported_format?(format, IMAGE_FORMATS)
732
924
  'image'
733
- when self.supported_format?(format, VIDEO_FORMATS)
925
+ when self.supported_format?(format, VIDEO_FORMATS), self.supported_format?(format, AUDIO_FORMATS)
734
926
  'video'
735
- when self.supported_format?(format, AUDIO_FORMATS)
736
- 'audio'
737
927
  else
738
928
  'raw'
739
929
  end
@@ -741,7 +931,8 @@ class Cloudinary::Utils
741
931
 
742
932
  def self.config_option_consume(options, option_name, default_value = nil)
743
933
  return options.delete(option_name) if options.include?(option_name)
744
- return Cloudinary.config.send(option_name) || default_value
934
+ option_value = Cloudinary.config.send(option_name)
935
+ option_value.nil? ? default_value : option_value
745
936
  end
746
937
 
747
938
  def self.as_bool(value)
@@ -750,7 +941,7 @@ class Cloudinary::Utils
750
941
  when String then value.downcase == "true" || value == "1"
751
942
  when TrueClass then true
752
943
  when FalseClass then false
753
- when Fixnum then value != 0
944
+ when Integer then value != 0
754
945
  when Symbol then value == :true
755
946
  else
756
947
  raise "Invalid boolean value #{value} of type #{value.class}"
@@ -827,6 +1018,7 @@ class Cloudinary::Utils
827
1018
  :keep_derived=>Cloudinary::Utils.as_safe_bool(options[:keep_derived]),
828
1019
  :tags=>options[:tags] && Cloudinary::Utils.build_array(options[:tags]),
829
1020
  :public_ids=>options[:public_ids] && Cloudinary::Utils.build_array(options[:public_ids]),
1021
+ :fully_qualified_public_ids=>options[:fully_qualified_public_ids] && Cloudinary::Utils.build_array(options[:fully_qualified_public_ids]),
830
1022
  :prefixes=>options[:prefixes] && Cloudinary::Utils.build_array(options[:prefixes]),
831
1023
  :expires_at=>options[:expires_at],
832
1024
  :transformations => build_eager(options[:transformations]),
@@ -968,4 +1160,82 @@ class Cloudinary::Utils
968
1160
  end
969
1161
  private_class_method :process_video_params
970
1162
 
1163
+ def self.process_custom_pre_function(param)
1164
+ value = process_custom_function(param)
1165
+ value ? "pre:#{value}" : nil
1166
+ end
1167
+
1168
+ def self.process_custom_function(param)
1169
+ return param unless param.is_a? Hash
1170
+
1171
+ function_type = param[:function_type]
1172
+ source = param[:source]
1173
+
1174
+ source = Base64.urlsafe_encode64(source) if function_type == "remote"
1175
+ "#{function_type}:#{source}"
1176
+ end
1177
+
1178
+ #
1179
+ # Handle the format parameter for fetch urls
1180
+ # @private
1181
+ # @param options url and transformation options. This argument may be changed by the function!
1182
+ #
1183
+ def self.patch_fetch_format(options={})
1184
+ if options[:type] === :fetch
1185
+ format_arg = options.delete(:format)
1186
+ options[:fetch_format] ||= format_arg
1187
+ end
1188
+ end
1189
+
1190
+ def self.is_remote?(url)
1191
+ REMOTE_URL_REGEX === url
1192
+ end
1193
+
1194
+ # The returned url should allow downloading the backedup asset based on the version and asset id
1195
+ #
1196
+ # asset and version id are returned with resource(<PUBLIC_ID1>, { versions: true })
1197
+ #
1198
+ # @param [String] asset_id Asset identifier
1199
+ # @param [String] version_id Specific version of asset to download
1200
+ # @param [Hash] options Additional options
1201
+ #
1202
+ # @return [String] An url for downloading a file
1203
+ def self.download_backedup_asset(asset_id, version_id, options = {})
1204
+ params = Cloudinary::Utils.sign_request({
1205
+ :timestamp => (options[:timestamp] || Time.now.to_i),
1206
+ :asset_id => asset_id,
1207
+ :version_id => version_id
1208
+ }, options)
1209
+
1210
+ "#{Cloudinary::Utils.base_api_url("download_backup", options)}?#{Cloudinary::Utils.hash_query_params((params))}"
1211
+ end
1212
+
1213
+ # Format date in a format accepted by the usage API (e.g., 31-12-2020) if
1214
+ # passed value is of type Date, otherwise return the string representation of
1215
+ # the input.
1216
+ #
1217
+ # @param [Date|Object] date
1218
+ # @return [String]
1219
+ def self.to_usage_api_date_format(date)
1220
+ if date.is_a?(Date)
1221
+ date.strftime('%d-%m-%Y')
1222
+ else
1223
+ date.to_s
1224
+ end
1225
+ end
1226
+
1227
+ # Computes hash from input string using specified algorithm.
1228
+ #
1229
+ # @param [String] input String which to compute hash from
1230
+ # @param [String|nil] signature_algorithm Algorithm to use for computing hash
1231
+ # @param [Symbol] hash_method Hash method applied to a signature algorithm (:digest or :hexdigest)
1232
+ #
1233
+ # @return [String] Computed hash value
1234
+ def self.hash(input, signature_algorithm = nil, hash_method = :digest)
1235
+ signature_algorithm ||= Cloudinary.config.signature_algorithm || ALGO_SHA1
1236
+ algorithm = ALGORITHM_SIGNATURE[signature_algorithm] || raise("Unsupported algorithm '#{signature_algorithm}'")
1237
+ algorithm.public_send(hash_method, input)
1238
+ end
1239
+
1240
+ private_class_method :hash
971
1241
  end