cloudinary 1.9.1 → 1.20.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.
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