cloudinary 1.9.1 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.encode_hash(context),
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
@@ -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
- unless public_id.blank?
234
- public_id = public_id.gsub("/", ":")
235
- public_id = "#{public_id}.#{format}" unless format.nil?
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
@@ -1,4 +1,4 @@
1
1
  # Copyright Cloudinary
2
2
  module Cloudinary
3
- VERSION = "1.9.1"
3
+ VERSION = "1.10.0"
4
4
  end
@@ -9,6 +9,9 @@ def days(n)
9
9
  end
10
10
 
11
11
  describe "Access Control" do
12
+ before :each do
13
+ Cloudinary.reset_config
14
+ end
12
15
  let (:acl) {{
13
16
  :access_type => 'anonymous',
14
17
  :start => '2019-02-22 16:20:57 +0200',
@@ -202,9 +202,9 @@ describe Cloudinary::Api do
202
202
 
203
203
  describe 'transformations' do
204
204
  it "should allow listing transformations" do
205
- transformation = @api.transformations(max_results: 500)["transformations"].find { |transformation| transformation["name"] == TEST_TRANSFOMATION }
206
- expect(transformation).not_to be_blank
207
- expect(transformation["used"]).to eq(true)
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
@@ -3,25 +3,23 @@ require 'spec_helper'
3
3
  require 'cloudinary'
4
4
 
5
5
  describe 'auth_token' do
6
- before :all do
7
- @url_backup = ENV["CLOUDINARY_URL"]
8
- end
9
- before do
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
@@ -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