cloudinary 1.9.1 → 1.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -5
- data/.gitignore +1 -0
- data/CHANGELOG.md +32 -0
- data/cloudinary.gemspec +2 -1
- data/lib/cloudinary/api.rb +28 -13
- 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/helper.rb +47 -4
- data/lib/cloudinary/responsive.rb +111 -0
- data/lib/cloudinary/uploader.rb +22 -4
- data/lib/cloudinary/utils.rb +122 -5
- data/lib/cloudinary/version.rb +1 -1
- data/spec/access_control_spec.rb +3 -0
- data/spec/api_spec.rb +19 -13
- data/spec/auth_token_spec.rb +8 -10
- data/spec/cache_spec.rb +109 -0
- data/spec/cloudinary_helper_spec.rb +147 -12
- data/spec/image_spec.rb +107 -0
- data/spec/spec_helper.rb +38 -17
- data/spec/uploader_spec.rb +23 -8
- data/spec/utils_spec.rb +46 -3
- data/spec/video_tag_spec.rb +7 -5
- data/spec/video_url_spec.rb +4 -2
- data/tools/update_version +202 -0
- metadata +83 -30
data/lib/cloudinary/uploader.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Copyright Cloudinary
|
2
2
|
require 'rest_client'
|
3
3
|
require 'json'
|
4
|
+
require 'cloudinary/cache'
|
4
5
|
|
5
6
|
class Cloudinary::Uploader
|
6
7
|
|
@@ -100,6 +101,7 @@ class Cloudinary::Uploader
|
|
100
101
|
else
|
101
102
|
filename = "cloudinaryfile"
|
102
103
|
end
|
104
|
+
unique_upload_id = Cloudinary::Utils.random_public_id
|
103
105
|
upload = nil
|
104
106
|
index = 0
|
105
107
|
chunk_size = options[:chunk_size] || 20_000_000
|
@@ -107,8 +109,7 @@ class Cloudinary::Uploader
|
|
107
109
|
buffer = file.read(chunk_size)
|
108
110
|
current_loc = index*chunk_size
|
109
111
|
range = "bytes #{current_loc}-#{current_loc+buffer.size - 1}/#{file.size}"
|
110
|
-
upload = upload_large_part(Cloudinary::Blob.new(buffer, :original_filename => filename), options.merge(:public_id => public_id, :content_range => range))
|
111
|
-
public_id = upload["public_id"]
|
112
|
+
upload = upload_large_part(Cloudinary::Blob.new(buffer, :original_filename => filename), options.merge(:public_id => public_id, :unique_upload_id => unique_upload_id, :content_range => range))
|
112
113
|
index += 1
|
113
114
|
end
|
114
115
|
upload
|
@@ -292,7 +293,7 @@ class Cloudinary::Uploader
|
|
292
293
|
return call_api("context", options) do
|
293
294
|
{
|
294
295
|
:timestamp => (options[:timestamp] || Time.now.to_i),
|
295
|
-
:context => Cloudinary::Utils.
|
296
|
+
:context => Cloudinary::Utils.encode_context(context),
|
296
297
|
:public_ids => Cloudinary::Utils.build_array(public_ids),
|
297
298
|
:command => command,
|
298
299
|
:type => options[:type]
|
@@ -303,6 +304,7 @@ class Cloudinary::Uploader
|
|
303
304
|
def self.call_api(action, options)
|
304
305
|
options = options.clone
|
305
306
|
return_error = options.delete(:return_error)
|
307
|
+
use_cache = options[:use_cache] || Cloudinary.config.use_cache
|
306
308
|
|
307
309
|
params, non_signable = yield
|
308
310
|
non_signable ||= []
|
@@ -320,6 +322,7 @@ class Cloudinary::Uploader
|
|
320
322
|
api_url = Cloudinary::Utils.cloudinary_api_url(action, options)
|
321
323
|
headers = { "User-Agent" => Cloudinary::USER_AGENT }
|
322
324
|
headers['Content-Range'] = options[:content_range] if options[:content_range]
|
325
|
+
headers['X-Unique-Upload-Id'] = options[:unique_upload_id] if options[:unique_upload_id]
|
323
326
|
headers.merge!(options[:extra_headers]) if options[:extra_headers]
|
324
327
|
RestClient::Request.execute(:method => :post, :url => api_url, :payload => params.reject { |k, v| v.nil? || v=="" }, :timeout => timeout, :headers => headers) do
|
325
328
|
|response, request, tmpresult|
|
@@ -338,11 +341,26 @@ class Cloudinary::Uploader
|
|
338
341
|
end
|
339
342
|
end
|
340
343
|
end
|
341
|
-
|
344
|
+
if use_cache && !result.nil?
|
345
|
+
cache_results(result)
|
346
|
+
end
|
342
347
|
result
|
343
348
|
end
|
344
349
|
|
345
350
|
def self.build_custom_headers(headers)
|
346
351
|
Array(headers).map { |*a| a.join(": ") }.join("\n")
|
347
352
|
end
|
353
|
+
|
354
|
+
def self.cache_results(result)
|
355
|
+
if result["responsive_breakpoints"]
|
356
|
+
result["responsive_breakpoints"].each do |bp|
|
357
|
+
Cloudinary::Cache.set(
|
358
|
+
result["public_id"],
|
359
|
+
{type: result["type"], resource_type: result["resource_type"], raw_transformation: bp["transformation"]},
|
360
|
+
bp["breakpoints"].map{|o| o['width']}
|
361
|
+
)
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
end
|
348
366
|
end
|
data/lib/cloudinary/utils.rb
CHANGED
@@ -6,6 +6,7 @@ require 'aws_cf_signer'
|
|
6
6
|
require 'json'
|
7
7
|
require 'cgi'
|
8
8
|
require 'cloudinary/auth_token'
|
9
|
+
require 'cloudinary/responsive'
|
9
10
|
|
10
11
|
class Cloudinary::Utils
|
11
12
|
# @deprecated Use Cloudinary::SHARED_CDN
|
@@ -40,13 +41,100 @@ class Cloudinary::Utils
|
|
40
41
|
"tags" => "tags",
|
41
42
|
"width" => "w"
|
42
43
|
}
|
44
|
+
|
45
|
+
URL_KEYS = %w[
|
46
|
+
api_secret
|
47
|
+
auth_token
|
48
|
+
cdn_subdomain
|
49
|
+
cloud_name
|
50
|
+
cname
|
51
|
+
format
|
52
|
+
private_cdn
|
53
|
+
resource_type
|
54
|
+
secure
|
55
|
+
secure_cdn_subdomain
|
56
|
+
secure_distribution
|
57
|
+
shorten
|
58
|
+
sign_url
|
59
|
+
ssl_detected
|
60
|
+
type
|
61
|
+
url_suffix
|
62
|
+
use_root_path
|
63
|
+
version
|
64
|
+
].map(&:to_sym)
|
65
|
+
|
66
|
+
|
67
|
+
TRANSFORMATION_PARAMS = %w[
|
68
|
+
angle
|
69
|
+
aspect_ratio
|
70
|
+
audio_codec
|
71
|
+
audio_frequency
|
72
|
+
background
|
73
|
+
bit_rate
|
74
|
+
border
|
75
|
+
color
|
76
|
+
color_space
|
77
|
+
crop
|
78
|
+
custom_function
|
79
|
+
default_image
|
80
|
+
delay
|
81
|
+
density
|
82
|
+
dpr
|
83
|
+
duration
|
84
|
+
effect
|
85
|
+
end_offset
|
86
|
+
fetch_format
|
87
|
+
flags
|
88
|
+
gravity
|
89
|
+
height
|
90
|
+
if
|
91
|
+
keyframe_interval
|
92
|
+
offset
|
93
|
+
opacity
|
94
|
+
overlay
|
95
|
+
page
|
96
|
+
prefix
|
97
|
+
quality
|
98
|
+
radius
|
99
|
+
raw_transformation
|
100
|
+
responsive_width
|
101
|
+
size
|
102
|
+
start_offset
|
103
|
+
streaming_profile
|
104
|
+
transformation
|
105
|
+
underlay
|
106
|
+
variables
|
107
|
+
video_codec
|
108
|
+
video_sampling
|
109
|
+
width
|
110
|
+
x
|
111
|
+
y
|
112
|
+
zoom
|
113
|
+
].map(&:to_sym)
|
114
|
+
|
115
|
+
def self.extract_config_params(options)
|
116
|
+
options.select{|k,v| URL_KEYS.include?(k)}
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.extract_transformation_params(options)
|
120
|
+
options.select{|k,v| TRANSFORMATION_PARAMS.include?(k)}
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.chain_transformation(options, *transformation)
|
124
|
+
base_options = extract_config_params(options)
|
125
|
+
transformation = transformation.reject(&:nil?)
|
126
|
+
base_options[:transformation] = build_array(extract_transformation_params(options)).concat(transformation)
|
127
|
+
base_options
|
128
|
+
end
|
129
|
+
|
130
|
+
|
43
131
|
# Warning: options are being destructively updated!
|
44
132
|
def self.generate_transformation_string(options={}, allow_implicit_crop_mode = false)
|
45
133
|
# allow_implicit_crop_mode was added to support height and width parameters without specifying a crop mode.
|
46
134
|
# This only apply to this (cloudinary_gem) SDK
|
47
135
|
|
48
136
|
if options.is_a?(Array)
|
49
|
-
return options.map{|base_transformation| generate_transformation_string(base_transformation.clone, allow_implicit_crop_mode)}.join("/")
|
137
|
+
return options.map{|base_transformation| generate_transformation_string(base_transformation.clone, allow_implicit_crop_mode)}.reject(&:blank?).join("/")
|
50
138
|
end
|
51
139
|
|
52
140
|
symbolize_keys!(options)
|
@@ -105,6 +193,7 @@ class Cloudinary::Utils
|
|
105
193
|
overlay = process_layer(options.delete(:overlay))
|
106
194
|
underlay = process_layer(options.delete(:underlay))
|
107
195
|
ifValue = process_if(options.delete(:if))
|
196
|
+
custom_function = process_custom_function(options.delete(:custom_function))
|
108
197
|
|
109
198
|
params = {
|
110
199
|
:a => normalize_expression(angle),
|
@@ -116,6 +205,7 @@ class Cloudinary::Utils
|
|
116
205
|
:dpr => normalize_expression(dpr),
|
117
206
|
:e => normalize_expression(effect),
|
118
207
|
:fl => flags,
|
208
|
+
:fn => custom_function,
|
119
209
|
:h => normalize_expression(height),
|
120
210
|
:l => overlay,
|
121
211
|
:o => normalize_expression(options.delete(:opacity)),
|
@@ -230,9 +320,13 @@ class Cloudinary::Utils
|
|
230
320
|
text_style = nil
|
231
321
|
components = []
|
232
322
|
|
233
|
-
|
234
|
-
|
235
|
-
|
323
|
+
if public_id.present?
|
324
|
+
if type == "fetch" && public_id.match(%r(^https?:/)i)
|
325
|
+
public_id = Base64.urlsafe_encode64(public_id)
|
326
|
+
else
|
327
|
+
public_id = public_id.gsub("/", ":")
|
328
|
+
public_id = "#{public_id}.#{format}" if format
|
329
|
+
end
|
236
330
|
end
|
237
331
|
|
238
332
|
if text.blank? && resource_type != "text"
|
@@ -348,9 +442,9 @@ class Cloudinary::Utils
|
|
348
442
|
# Warning: options are being destructively updated!
|
349
443
|
def self.unsigned_download_url(source, options = {})
|
350
444
|
|
445
|
+
patch_fetch_format(options)
|
351
446
|
type = options.delete(:type)
|
352
447
|
|
353
|
-
options[:fetch_format] ||= options.delete(:format) if type.to_s == "fetch"
|
354
448
|
transformation = self.generate_transformation_string(options)
|
355
449
|
|
356
450
|
resource_type = options.delete(:resource_type)
|
@@ -410,6 +504,7 @@ class Cloudinary::Utils
|
|
410
504
|
|
411
505
|
transformation = transformation.gsub(%r(([^:])//), '\1/')
|
412
506
|
if sign_url && ( !auth_token || auth_token.empty?)
|
507
|
+
raise(CloudinaryException, "Must supply api_secret") if (secret.nil? || secret.empty?)
|
413
508
|
to_sign = [transformation, sign_version && version, source_to_sign].reject(&:blank?).join("/")
|
414
509
|
to_sign = fully_unescape(to_sign)
|
415
510
|
signature = 's--' + Base64.urlsafe_encode64(Digest::SHA1.digest(to_sign + secret))[0,8] + '--'
|
@@ -968,4 +1063,26 @@ class Cloudinary::Utils
|
|
968
1063
|
end
|
969
1064
|
private_class_method :process_video_params
|
970
1065
|
|
1066
|
+
def self.process_custom_function(param)
|
1067
|
+
return param unless param.is_a? Hash
|
1068
|
+
|
1069
|
+
function_type = param[:function_type]
|
1070
|
+
source = param[:source]
|
1071
|
+
|
1072
|
+
source = Base64.urlsafe_encode64(source) if function_type == "remote"
|
1073
|
+
"#{function_type}:#{source}"
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
#
|
1077
|
+
# Handle the format parameter for fetch urls
|
1078
|
+
# @private
|
1079
|
+
# @param options url and transformation options. This argument may be changed by the function!
|
1080
|
+
#
|
1081
|
+
def self.patch_fetch_format(options={})
|
1082
|
+
if options[:type] === :fetch
|
1083
|
+
format_arg = options.delete(:format)
|
1084
|
+
options[:fetch_format] ||= format_arg
|
1085
|
+
end
|
1086
|
+
end
|
1087
|
+
|
971
1088
|
end
|
data/lib/cloudinary/version.rb
CHANGED
data/spec/access_control_spec.rb
CHANGED
data/spec/api_spec.rb
CHANGED
@@ -202,9 +202,9 @@ describe Cloudinary::Api do
|
|
202
202
|
|
203
203
|
describe 'transformations' do
|
204
204
|
it "should allow listing transformations" do
|
205
|
-
|
206
|
-
expect(
|
207
|
-
expect(
|
205
|
+
transformations = @api.transformations()["transformations"]
|
206
|
+
expect(transformations[0]).not_to be_empty
|
207
|
+
expect(transformations[0]["used"]).to eq(true)
|
208
208
|
end
|
209
209
|
|
210
210
|
it "should allow getting transformation metadata" do
|
@@ -269,7 +269,7 @@ describe Cloudinary::Api do
|
|
269
269
|
expect(RestClient::Request).to receive(:execute).with(deep_hash_value( [:payload, :named ]=> true))
|
270
270
|
@api.transformations :named => true
|
271
271
|
end
|
272
|
-
|
272
|
+
|
273
273
|
end
|
274
274
|
it "should allow deleting implicit transformation" do
|
275
275
|
@api.transformation(TEST_TRANSFOMATION)
|
@@ -467,12 +467,12 @@ describe Cloudinary::Api do
|
|
467
467
|
|
468
468
|
expect(result["published"]).to be_an_instance_of(Array)
|
469
469
|
expect(result["published"].length).to eq(1)
|
470
|
-
|
470
|
+
|
471
471
|
resource = result["published"][0]
|
472
|
-
|
472
|
+
|
473
473
|
expect(resource["public_id"]).to eq(publicId)
|
474
474
|
expect(resource["type"]).to eq('upload')
|
475
|
-
|
475
|
+
|
476
476
|
bytes = resource["bytes"]
|
477
477
|
end
|
478
478
|
it "should publish resources by prefix and overwrite" do
|
@@ -480,13 +480,13 @@ describe Cloudinary::Api do
|
|
480
480
|
|
481
481
|
expect(result["published"]).to be_an_instance_of(Array)
|
482
482
|
expect(result["published"].length).to eq(1)
|
483
|
-
|
483
|
+
|
484
484
|
resource = result["published"][0]
|
485
|
-
|
485
|
+
|
486
486
|
expect(resource["public_id"]).to eq(publicId)
|
487
487
|
expect(resource["bytes"]).not_to eq(bytes)
|
488
488
|
expect(resource["type"]).to eq('upload')
|
489
|
-
|
489
|
+
|
490
490
|
bytes = resource["bytes"]
|
491
491
|
end
|
492
492
|
it "should publish resources by tag and overwrite" do
|
@@ -494,16 +494,22 @@ describe Cloudinary::Api do
|
|
494
494
|
|
495
495
|
expect(result["published"]).to be_an_instance_of(Array)
|
496
496
|
expect(result["published"].length).to eq(1)
|
497
|
-
|
497
|
+
|
498
498
|
resource = result["published"][0]
|
499
|
-
|
499
|
+
|
500
500
|
expect(resource["public_id"]).to eq(publicId)
|
501
501
|
expect(resource["bytes"]).not_to eq(bytes)
|
502
502
|
expect(resource["type"]).to eq('upload')
|
503
|
-
|
503
|
+
|
504
504
|
bytes = resource["bytes"]
|
505
505
|
end
|
506
506
|
end
|
507
|
+
describe "json breakpoints" do
|
508
|
+
it "should retrieve breakpoints as json array" do
|
509
|
+
bp = Cloudinary::Api.get_breakpoints(test_id_1, srcset: {min_width:10, max_width:2000, bytes_step: 10, max_images: 20})
|
510
|
+
expect(bp).to be_truthy
|
511
|
+
end
|
512
|
+
end
|
507
513
|
end
|
508
514
|
|
509
515
|
describe Cloudinary::Api::Response do
|
data/spec/auth_token_spec.rb
CHANGED
@@ -3,25 +3,23 @@ require 'spec_helper'
|
|
3
3
|
require 'cloudinary'
|
4
4
|
|
5
5
|
describe 'auth_token' do
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
Cloudinary.config_from_url "cloudinary://a:b@test123"
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
Cloudinary.reset_config
|
9
|
+
Cloudinary.config_from_url 'cloudinary://a:b@test123'
|
11
10
|
Cloudinary.config.auth_token = { :key => KEY, :duration => 300, :start_time => 11111111 }
|
12
11
|
end
|
13
|
-
after do
|
14
|
-
ENV["CLOUDINARY_URL"] = @url_backup
|
15
|
-
Cloudinary::config_from_url @url_backup
|
16
|
-
end
|
17
12
|
it "should generate with start and duration" do
|
18
13
|
token = Cloudinary::Utils.generate_auth_token :start_time => 1111111111, :acl => "/image/*", :duration => 300
|
19
14
|
expect(token).to eq '__cld_token__=st=1111111111~exp=1111111411~acl=%2fimage%2f*~hmac=1751370bcc6cfe9e03f30dd1a9722ba0f2cdca283fa3e6df3342a00a7528cc51'
|
20
15
|
end
|
21
16
|
|
22
17
|
describe "authenticated url" do
|
23
|
-
before do
|
18
|
+
before :each do
|
19
|
+
Cloudinary.class_variable_set :@@config, nil
|
20
|
+
Cloudinary.config_from_url 'cloudinary://a:b@test123'
|
24
21
|
Cloudinary.config :private_cdn => true
|
22
|
+
Cloudinary.config.auth_token = { :key => KEY, :duration => 300, :start_time => 11111111 }
|
25
23
|
|
26
24
|
end
|
27
25
|
it "should add token if authToken is globally set and signed = true" do
|
data/spec/cache_spec.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'cloudinary'
|
3
|
+
require 'cloudinary/cache'
|
4
|
+
require 'rspec'
|
5
|
+
require 'active_support/cache'
|
6
|
+
|
7
|
+
describe 'Responsive cache' do
|
8
|
+
|
9
|
+
before :all do
|
10
|
+
Cloudinary.reset_config
|
11
|
+
unless defined? Rails and defined? Rails.cache
|
12
|
+
module Rails
|
13
|
+
class << self
|
14
|
+
attr_accessor :cache
|
15
|
+
end
|
16
|
+
Rails.cache = ActiveSupport::Cache::FileStore.new("#{Dir.getwd}/../tmp/cache")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Cloudinary::config.use_cache = true
|
21
|
+
Cloudinary::Cache.storage= Rails.cache
|
22
|
+
@i=0
|
23
|
+
end
|
24
|
+
|
25
|
+
after :all do
|
26
|
+
# Rails.cache.clear
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_cache
|
30
|
+
Rails.cache.fetch CACHE_KEY do
|
31
|
+
@i = @i + 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
it 'should cache breakpoints' do
|
35
|
+
j = get_cache
|
36
|
+
j = get_cache
|
37
|
+
|
38
|
+
expect(j).to eql(1)
|
39
|
+
expect(@i).to eql(1)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should cache upload results' do
|
43
|
+
result = Cloudinary::Uploader.upload(
|
44
|
+
TEST_IMG,
|
45
|
+
:tags => [TEST_TAG, TIMESTAMP_TAG],
|
46
|
+
responsive_breakpoints: [
|
47
|
+
{
|
48
|
+
create_derived: false,
|
49
|
+
transformation: {
|
50
|
+
angle: 90
|
51
|
+
},
|
52
|
+
format: 'gif'
|
53
|
+
},
|
54
|
+
{
|
55
|
+
create_derived: false,
|
56
|
+
transformation: {angle: 45, crop: 'scale'},
|
57
|
+
format: 'png'
|
58
|
+
},
|
59
|
+
{
|
60
|
+
create_derived: false,
|
61
|
+
}
|
62
|
+
]
|
63
|
+
)
|
64
|
+
expect(result["responsive_breakpoints"]).to_not be_nil
|
65
|
+
expect(result["responsive_breakpoints"].length).to_not eql(0)
|
66
|
+
result["responsive_breakpoints"].each do |bp|
|
67
|
+
bp = Cloudinary::Cache.get(
|
68
|
+
result["public_id"],
|
69
|
+
{
|
70
|
+
type: bp["type"],
|
71
|
+
resource_type: bp["resource_type"],
|
72
|
+
raw_transformation: bp["transformation"]})
|
73
|
+
expect(bp).to_not be_nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
describe Cloudinary::Uploader do
|
77
|
+
let (:options) { {
|
78
|
+
:tags => [TEST_TAG, TIMESTAMP_TAG],
|
79
|
+
:use_cache => true,
|
80
|
+
:responsive_breakpoints => [
|
81
|
+
{
|
82
|
+
:create_derived => false,
|
83
|
+
:transformation => {
|
84
|
+
:angle => 90
|
85
|
+
},
|
86
|
+
:format => 'gif'
|
87
|
+
},
|
88
|
+
{
|
89
|
+
:create_derived => false,
|
90
|
+
:transformation => ResponsiveTest::TRANSFORMATION,
|
91
|
+
:format => ResponsiveTest::FORMAT
|
92
|
+
},
|
93
|
+
{
|
94
|
+
:create_derived => false
|
95
|
+
}
|
96
|
+
]
|
97
|
+
}}
|
98
|
+
before :all do
|
99
|
+
end
|
100
|
+
|
101
|
+
it "Should save responsive breakpoints to cache after upload" do
|
102
|
+
result = Cloudinary::Uploader.upload( TEST_IMG, options)
|
103
|
+
cache_value = Cloudinary::Cache.get(result["public_id"], transformation: ResponsiveTest::TRANSFORMATION, format: ResponsiveTest::FORMAT)
|
104
|
+
|
105
|
+
expect(cache_value).to eql(ResponsiveTest::IMAGE_BP_VALUES)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|