cloudinary 1.9.1 → 1.20.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|