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.
- checksums.yaml +5 -5
- data/.github/ISSUE_TEMPLATE/bug_report.md +42 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
- data/.github/pull_request_template.md +24 -0
- data/.gitignore +7 -1
- data/.travis.yml +15 -8
- data/CHANGELOG.md +261 -0
- data/README.md +3 -0
- data/Rakefile +3 -45
- data/cloudinary.gemspec +27 -20
- data/lib/active_storage/blob_key.rb +20 -0
- data/lib/active_storage/service/cloudinary_service.rb +249 -0
- data/lib/cloudinary.rb +53 -63
- data/lib/cloudinary/account_api.rb +231 -0
- data/lib/cloudinary/account_config.rb +30 -0
- data/lib/cloudinary/api.rb +228 -71
- data/lib/cloudinary/auth_token.rb +10 -4
- data/lib/cloudinary/base_api.rb +79 -0
- data/lib/cloudinary/base_config.rb +70 -0
- data/lib/cloudinary/cache.rb +38 -0
- data/lib/cloudinary/cache/breakpoints_cache.rb +31 -0
- data/lib/cloudinary/cache/key_value_cache_adapter.rb +25 -0
- data/lib/cloudinary/cache/rails_cache_adapter.rb +34 -0
- data/lib/cloudinary/cache/storage/rails_cache_storage.rb +5 -0
- data/lib/cloudinary/carrier_wave.rb +4 -2
- data/lib/cloudinary/carrier_wave/remote.rb +3 -2
- data/lib/cloudinary/carrier_wave/storage.rb +2 -1
- data/lib/cloudinary/{controller.rb → cloudinary_controller.rb} +3 -5
- data/lib/cloudinary/config.rb +43 -0
- data/lib/cloudinary/helper.rb +77 -7
- data/lib/cloudinary/migrator.rb +3 -1
- data/lib/cloudinary/railtie.rb +7 -3
- data/lib/cloudinary/responsive.rb +111 -0
- data/lib/cloudinary/uploader.rb +67 -15
- data/lib/cloudinary/utils.rb +324 -54
- data/lib/cloudinary/version.rb +1 -1
- data/lib/cloudinary/video_helper.rb +96 -22
- data/lib/tasks/cloudinary/fetch_assets.rake +48 -0
- data/lib/tasks/{cloudinary.rake → cloudinary/sync_static.rake} +0 -0
- data/tools/allocate_test_cloud.sh +9 -0
- data/tools/get_test_cloud.sh +9 -0
- data/tools/update_version +220 -0
- data/vendor/assets/javascripts/cloudinary/jquery.cloudinary.js +51 -13
- data/vendor/assets/javascripts/cloudinary/jquery.fileupload.js +24 -4
- data/vendor/assets/javascripts/cloudinary/jquery.ui.widget.js +741 -561
- data/vendor/assets/javascripts/cloudinary/load-image.all.min.js +1 -1
- metadata +92 -67
- data/spec/access_control_spec.rb +0 -99
- data/spec/api_spec.rb +0 -545
- data/spec/archive_spec.rb +0 -129
- data/spec/auth_token_spec.rb +0 -79
- data/spec/cloudinary_helper_spec.rb +0 -190
- data/spec/cloudinary_spec.rb +0 -32
- data/spec/data/sync_static/app/assets/javascripts/1.coffee +0 -1
- data/spec/data/sync_static/app/assets/javascripts/1.js +0 -1
- data/spec/data/sync_static/app/assets/stylesheets/1.css +0 -3
- data/spec/docx.docx +0 -0
- data/spec/favicon.ico +0 -0
- data/spec/logo.png +0 -0
- data/spec/rake_spec.rb +0 -160
- data/spec/sample_asset_file.tsv +0 -4
- data/spec/search_spec.rb +0 -109
- data/spec/spec_helper.rb +0 -245
- data/spec/storage_spec.rb +0 -44
- data/spec/streaminig_profiles_api_spec.rb +0 -74
- data/spec/support/helpers/temp_file_helpers.rb +0 -22
- data/spec/support/shared_contexts/rake.rb +0 -19
- data/spec/uploader_spec.rb +0 -363
- data/spec/utils_methods_spec.rb +0 -54
- data/spec/utils_spec.rb +0 -906
- data/spec/video_tag_spec.rb +0 -251
- data/spec/video_url_spec.rb +0 -164
data/lib/cloudinary/utils.rb
CHANGED
@@ -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
|
-
"
|
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 =>
|
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(
|
201
|
-
|
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
|
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
|
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
|
-
|
234
|
-
|
235
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
533
|
-
|
534
|
-
resource_type
|
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] =
|
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::
|
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
|
-
|
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
|
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
|