cloudinary 1.0.32 → 1.0.33

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.
data/cloudinary.gemspec CHANGED
@@ -19,5 +19,6 @@ Gem::Specification.new do |s|
19
19
  s.require_paths = ["lib"]
20
20
 
21
21
  s.add_dependency "rest-client"
22
+ s.add_dependency "aws_cf_signer"
22
23
  s.add_development_dependency "rspec"
23
24
  end
data/lib/cloudinary.rb CHANGED
@@ -6,6 +6,7 @@ require "uri"
6
6
  require "cloudinary/version"
7
7
  require "cloudinary/utils"
8
8
  require "cloudinary/uploader"
9
+ require "cloudinary/api"
9
10
  require "cloudinary/downloader"
10
11
  require "cloudinary/blob"
11
12
  require "cloudinary/static"
@@ -41,6 +42,11 @@ module Cloudinary
41
42
  "private_cdn" => !uri.path.blank?,
42
43
  "secure_distribution" => uri.path[1..-1]
43
44
  )
45
+ uri.query.to_s.split("&").each do
46
+ |param|
47
+ key, value = param.split("=")
48
+ set_config(key=>URI.decode(value))
49
+ end
44
50
  end
45
51
 
46
52
  set_config(new_config) if new_config
@@ -0,0 +1,140 @@
1
+ class Cloudinary::Api
2
+ class Error < StandardError; end
3
+ class NotFound < Error; end
4
+ class NotAllowed < Error; end
5
+ class AlreadyExists < Error; end
6
+ class RateLimited < Error; end
7
+ class BadRequest < Error; end
8
+ class GeneralError < Error; end
9
+ class AuthorizationRequired < Error; end
10
+ class Response < Hash
11
+ attr_reader :rate_limit_reset_at, :rate_limit_remaining, :rate_limit_allowed
12
+ def initialize(response)
13
+ self.update(Cloudinary::Api.send(:parse_json_response, response))
14
+ @rate_limit_allowed = response.headers[:x_featureratelimit_limit].to_i
15
+ @rate_limit_reset_at = Time.parse(response.headers[:x_featureratelimit_reset])
16
+ @rate_limit_remaining = response.headers[:x_featureratelimit_remaining].to_i
17
+ end
18
+ end
19
+
20
+
21
+ def self.resource_types(options={})
22
+ call_api(:get, "resources", {}, options)
23
+ end
24
+
25
+ def self.resources(options={})
26
+ resource_type = options[:resource_type] || "image"
27
+ type = options[:type]
28
+ uri = "resources/#{resource_type}"
29
+ uri += "/#{type}" if !type.blank?
30
+ call_api(:get, uri, only(options, :next_cursor, :max_results, :prefix), options)
31
+ end
32
+
33
+ def self.resources_by_tag(tag, options={})
34
+ resource_type = options[:resource_type] || "image"
35
+ uri = "resources/#{resource_type}/tags/#{tag}"
36
+ call_api(:get, uri, only(options, :next_cursor, :max_results), options)
37
+ end
38
+
39
+ def self.resource(public_id, options={})
40
+ resource_type = options[:resource_type] || "image"
41
+ type = options[:type] || "upload"
42
+ uri = "resources/#{resource_type}/#{type}/#{public_id}"
43
+ call_api(:get, uri, {}, options)
44
+ end
45
+
46
+ def self.delete_resources(public_ids, options={})
47
+ resource_type = options[:resource_type] || "image"
48
+ type = options[:type] || "upload"
49
+ uri = "resources/#{resource_type}/#{type}"
50
+ call_api(:delete, uri, {:public_ids=>public_ids}, options)
51
+ end
52
+
53
+ def self.delete_resources_by_prefix(type, prefix, options={})
54
+ resource_type = options[:resource_type] || "image"
55
+ uri = "resources/#{resource_type}/#{type}"
56
+ call_api(:delete, uri, {:prefix=>prefix}, options)
57
+ end
58
+
59
+ def self.delete_derived_resources(derived_resource_ids, options={})
60
+ uri = "derived_resources"
61
+ call_api(:delete, uri, {:derived_resource_ids=>derived_resource_ids}, options)
62
+ end
63
+
64
+ def self.tags(options={})
65
+ resource_type = options[:resource_type] || "image"
66
+ uri = "tags/#{resource_type}"
67
+ call_api(:get, uri, only(options, :next_cursor, :max_results, :prefix), options)
68
+ end
69
+
70
+ def self.transformations(options={})
71
+ call_api(:get, "transformations", only(options, :next_cursor, :max_results), options)
72
+ end
73
+
74
+ def self.transformation(transformation, options={})
75
+ call_api(:get, "transformations/#{transformation_string(transformation)}", only(options, :max_results), options)
76
+ end
77
+
78
+ def self.delete_transformation(transformation, options={})
79
+ call_api(:delete, "transformations/#{transformation_string(transformation)}", {}, options)
80
+ end
81
+
82
+ # updates - currently only supported update is the "allowed_for_strict" boolean flag
83
+ def self.update_transformation(transformation, updates, options={})
84
+ call_api(:put, "transformations/#{transformation_string(transformation)}", only(updates, :allowed_for_strict), options)
85
+ end
86
+
87
+ def self.create_transformation(name, definition, options={})
88
+ call_api(:post, "transformations/#{name}", {:transformation=>transformation_string(definition)}, options)
89
+ end
90
+
91
+ protected
92
+
93
+ def self.call_api(method, uri, params, options)
94
+ cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || "https://api.cloudinary.com"
95
+ cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise("Must supply cloud_name")
96
+ api_key = options[:api_key] || Cloudinary.config.api_key || raise("Must supply api_key")
97
+ api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise("Must supply api_secret")
98
+ api_url = [cloudinary, "v1_1", cloud_name, uri].join("/")
99
+ # Add authentication
100
+ api_url.sub!(%r(^(https?://)), "\\1#{api_key}:#{api_secret}@")
101
+
102
+ RestClient::Request.execute(:method => method, :url => api_url, :payload => params.reject{|k, v| v.nil? || v==""}, :timeout=>60) do
103
+ |response, request, tmpresult|
104
+ return Response.new(response) if response.code == 200
105
+ exception_class = case response.code
106
+ when 400 then BadRequest
107
+ when 401 then AuthorizationRequired
108
+ when 403 then NotAllowed
109
+ when 404 then NotFound
110
+ when 409 then AlreadyExists
111
+ when 420 then RateLimited
112
+ when 500 then GeneralError
113
+ else raise GeneralError.new("Server returned unexpected status code - #{response.code} - #{response.body}")
114
+ end
115
+ json = parse_json_response(response)
116
+ raise exception_class.new(json["error"]["message"])
117
+ end
118
+ end
119
+
120
+ def self.parse_json_response(response)
121
+ return Cloudinary::Utils.json_decode(response.body)
122
+ rescue => e
123
+ # Error is parsing json
124
+ raise GeneralError.new("Error parsing server response (#{response.code}) - #{response.body}. Got - #{e}")
125
+ end
126
+
127
+ def self.only(hash, *keys)
128
+ result = {}
129
+ keys.each do
130
+ |key|
131
+ result[key] = hash[key] if hash.include?(key)
132
+ result[key] = hash[key.to_s] if hash.include?(key.to_s)
133
+ end
134
+ result
135
+ end
136
+
137
+ def self.transformation_string(transformation)
138
+ transformation = transformation.is_a?(String) ? transformation : Cloudinary::Utils.generate_transformation_string(transformation.clone)
139
+ end
140
+ end
@@ -154,6 +154,10 @@ module CloudinaryHelper
154
154
  def cl_zip_download_url(tag, options = {})
155
155
  Cloudinary::Utils.zip_download_url(tag, options)
156
156
  end
157
+
158
+ def cl_signed_download_url(public_id, options = {})
159
+ Cloudinary::Utils.signed_download_url(public_id, options)
160
+ end
157
161
 
158
162
  def self.included(base)
159
163
  ActionView::Helpers::FormBuilder.send(:include, Cloudinary::FormBuilder)
@@ -1,12 +1,21 @@
1
1
  # Copyright Cloudinary
2
2
  require 'digest/sha1'
3
3
  require 'zlib'
4
+ require 'aws_cf_signer'
4
5
 
5
6
  class Cloudinary::Utils
6
7
  SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"
7
8
 
8
9
  # Warning: options are being destructively updated!
9
10
  def self.generate_transformation_string(options={})
11
+ if options.is_a?(Array)
12
+ return options.map{|base_transformation| generate_transformation_string(base_transformation.clone)}.join("/")
13
+ end
14
+ # Symbolize keys
15
+ options.keys.each do |key|
16
+ options[key.to_sym] = options.delete(key) if key.is_a?(String)
17
+ end
18
+
10
19
  size = options.delete(:size)
11
20
  options[:width], options[:height] = size.split("x") if size
12
21
  width = options[:width]
@@ -35,10 +44,12 @@ class Cloudinary::Utils
35
44
 
36
45
  effect = options.delete(:effect)
37
46
  effect = Array(effect).flatten.join(":") if effect.is_a?(Array) || effect.is_a?(Hash)
47
+
48
+ angle = build_array(options.delete(:angle)).join(".")
38
49
 
39
- params = {:w=>width, :h=>height, :t=>named_transformation, :c=>crop, :b=>background, :e=>effect}
50
+ params = {:w=>width, :h=>height, :t=>named_transformation, :c=>crop, :b=>background, :e=>effect, :a=>angle}
40
51
  { :x=>:x, :y=>:y, :r=>:radius, :d=>:default_image, :g=>:gravity, :q=>:quality,
41
- :p=>:prefix, :a=>:angle, :l=>:overlay, :u=>:underlay, :f=>:fetch_format, :dn=>:density, :pg=>:page
52
+ :p=>:prefix, :l=>:overlay, :u=>:underlay, :f=>:fetch_format, :dn=>:density, :pg=>:page
42
53
  }.each do
43
54
  |param, option|
44
55
  params[param] = options.delete(option)
@@ -144,7 +155,7 @@ class Cloudinary::Utils
144
155
  def self.private_download_url(public_id, format, options = {})
145
156
  api_key = options[:api_key] || Cloudinary.config.api_key || raise("Must supply api_key")
146
157
  api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise("Must supply api_secret")
147
- cloudinary_params = {:timestamp=>Time.now.to_i, :public_id=>public_id, :format=>format}.reject{|k, v| v.blank?}
158
+ cloudinary_params = {:timestamp=>Time.now.to_i, :public_id=>public_id, :format=>format, :type=>options[:type]}.reject{|k, v| v.blank?}
148
159
  cloudinary_params[:signature] = Cloudinary::Utils.api_sign_request(cloudinary_params, api_secret)
149
160
  cloudinary_params[:api_key] = api_key
150
161
  return Cloudinary::Utils.cloudinary_api_url("download", options) + "?" + cloudinary_params.to_query
@@ -159,7 +170,17 @@ class Cloudinary::Utils
159
170
  return Cloudinary::Utils.cloudinary_api_url("download_tag.zip", options) + "?" + cloudinary_params.to_query
160
171
  end
161
172
 
162
-
173
+ def self.signed_download_url(public_id, options = {})
174
+ aws_private_key_path = options[:aws_private_key_path] || Cloudinary.config.aws_private_key_path || raise("Must supply aws_private_key_path")
175
+ aws_key_pair_id = options[:aws_key_pair_id] || Cloudinary.config.aws_key_pair_id || raise("Must supply aws_key_pair_id")
176
+ authenticated_distribution = options[:authenticated_distribution] || Cloudinary.config.authenticated_distribution || raise("Must supply authenticated_distribution")
177
+ @signers ||= Hash.new{|h,k| path, id = k; h[k] = AwsCfSigner.new(path, id)}
178
+ signer = @signers[[aws_private_key_path, aws_key_pair_id]]
179
+ url = Cloudinary::Utils.cloudinary_url(public_id, options.merge(:secure=>true, :secure_distribution=>authenticated_distribution, :private_cdn=>true))
180
+ expires_at = options[:expires_at] || (Time.now+3600)
181
+ signer.sign(url, :ending => expires_at)
182
+ end
183
+
163
184
  def self.asset_file_name(path)
164
185
  data = Rails.root.join(path).read(:mode=>"rb")
165
186
  ext = path.extname
@@ -1,4 +1,4 @@
1
1
  # Copyright Cloudinary
2
2
  module Cloudinary
3
- VERSION = "1.0.32"
3
+ VERSION = "1.0.33"
4
4
  end
data/spec/api_spec.rb ADDED
@@ -0,0 +1,139 @@
1
+ require 'spec_helper'
2
+ require 'cloudinary'
3
+
4
+ describe Cloudinary::Api do
5
+ break puts("Please setup environment for api test to run") if Cloudinary.config.api_secret.blank?
6
+
7
+ before(:all) do
8
+ @api = Cloudinary::Api
9
+ Cloudinary::Uploader.upload("spec/logo.png", :public_id=>"api_test", :tags=>"api_test_tag", :eager=>[:width=>100,:crop=>:scale])
10
+ Cloudinary::Uploader.upload("spec/logo.png", :public_id=>"api_test2", :tags=>"api_test_tag", :eager=>[:width=>100,:crop=>:scale])
11
+ @api.delete_transformation("api_test_transformation") rescue nil
12
+ end
13
+
14
+ it "should allow listing resource_types" do
15
+ @api.resource_types()["resource_types"].should include("image")
16
+ end
17
+
18
+ it "should allow listing resources" do
19
+ resource = @api.resources()["resources"].find{|resource| resource["public_id"] == "api_test"}
20
+ resource.should_not be_blank
21
+ resource["type"].should == "upload"
22
+ end
23
+
24
+ it "should allow listing resources with cursor" do
25
+ result = @api.resources(:max_results=>1)
26
+ result["resources"].should_not be_blank
27
+ result["resources"].length.should == 1
28
+ result["next_cursor"].should_not be_blank
29
+ result2 = @api.resources(:max_results=>1, :next_cursor=>result["next_cursor"])
30
+ result2["resources"].should_not be_blank
31
+ result2["resources"].length.should == 1
32
+ result2["resources"][0]["public_id"].should_not == result["resources"][0]["public_id"]
33
+ end
34
+
35
+
36
+ it "should allow listing resources by type" do
37
+ resource = @api.resources(:type=>"upload")["resources"].find{|resource| resource["public_id"] == "api_test"}
38
+ resource.should_not be_blank
39
+ end
40
+
41
+ it "should allow listing resources by prefix" do
42
+ public_ids = @api.resources(:type=>"upload", :prefix=>"api_test")["resources"].map{|resource| resource["public_id"]}
43
+ public_ids.should include("api_test", "api_test2")
44
+ end
45
+
46
+ it "should allow listing resources by tag" do
47
+ resource = @api.resources_by_tag("api_test_tag")["resources"].find{|resource| resource["public_id"] == "api_test"}
48
+ resource.should_not be_blank
49
+ end
50
+
51
+ it "should allow get resource metadata" do
52
+ resource = @api.resource("api_test")
53
+ resource.should_not be_blank
54
+ resource["public_id"].should == "api_test"
55
+ resource["bytes"].should == 3381
56
+ resource["derived"].length.should == 1
57
+ end
58
+
59
+ it "should allow deleting derived resource" do
60
+ Cloudinary::Uploader.upload("spec/logo.png", :public_id=>"api_test3", :eager=>[:width=>101,:crop=>:scale])
61
+ resource = @api.resource("api_test3")
62
+ resource.should_not be_blank
63
+ resource["derived"].length.should == 1
64
+ derived_resource_id = resource["derived"][0]["id"]
65
+ @api.delete_derived_resources(derived_resource_id)
66
+ resource = @api.resource("api_test3")
67
+ resource.should_not be_blank
68
+ resource["derived"].length.should == 0
69
+ end
70
+
71
+ it "should allow deleting resources" do
72
+ Cloudinary::Uploader.upload("spec/logo.png", :public_id=>"api_test3")
73
+ resource = @api.resource("api_test3")
74
+ resource.should_not be_blank
75
+ @api.delete_resources(["apit_test", "api_test2", "api_test3"])
76
+ lambda{@api.resource("api_test3")}.should raise_error(Cloudinary::Api::NotFound)
77
+ end
78
+
79
+ it "should allow listing tags" do
80
+ tags = @api.tags()["tags"]
81
+ tags.should include('api_test_tag')
82
+ end
83
+
84
+ it "should allow listing tag by prefix" do
85
+ tags = @api.tags(:prefix=>"api_test")["tags"]
86
+ tags.should include('api_test_tag')
87
+ tags = @api.tags(:prefix=>"api_test_no_such_tag")["tags"]
88
+ tags.should be_blank
89
+ end
90
+
91
+ it "should allow listing transformations" do
92
+ transformation = @api.transformations()["transformations"].find{|transformation| transformation["name"] == "c_scale,w_100"}
93
+ transformation.should_not be_blank
94
+ transformation["used"].should == true
95
+ end
96
+
97
+ it "should allow getting transformation metadata" do
98
+ transformation = @api.transformation("c_scale,w_100")
99
+ transformation.should_not be_blank
100
+ transformation["info"].should == ["crop"=>"scale", "width"=>100]
101
+ transformation = @api.transformation("crop"=>"scale", "width"=>100)
102
+ transformation.should_not be_blank
103
+ transformation["info"].should == ["crop"=>"scale", "width"=>100]
104
+ end
105
+
106
+ it "should allow updating transformation allowed_for_strict" do
107
+ @api.update_transformation("c_scale,w_100", :allowed_for_strict=>true)
108
+ transformation = @api.transformation("c_scale,w_100")
109
+ transformation.should_not be_blank
110
+ transformation["allowed_for_strict"].should == true
111
+ @api.update_transformation("c_scale,w_100", :allowed_for_strict=>false)
112
+ transformation = @api.transformation("c_scale,w_100")
113
+ transformation.should_not be_blank
114
+ transformation["allowed_for_strict"].should == false
115
+ end
116
+
117
+ it "should allow creating named transformation" do
118
+ @api.create_transformation("api_test_transformation", "crop"=>"scale", "width"=>102)
119
+ transformation = @api.transformation("api_test_transformation")
120
+ transformation.should_not be_blank
121
+ transformation["allowed_for_strict"].should == true
122
+ transformation["info"].should == ["crop"=>"scale", "width"=>102]
123
+ transformation["used"].should == false
124
+ end
125
+
126
+ it "should allow deleting named transformation" do
127
+ @api.create_transformation("api_test_transformation2", "crop"=>"scale", "width"=>103)
128
+ @api.transformation("api_test_transformation2")
129
+ @api.delete_transformation("api_test_transformation2")
130
+ lambda{@api.transformation("api_test_transformation2")}.should raise_error(Cloudinary::Api::NotFound)
131
+ end
132
+
133
+ it "should allow deleting implicit transformation" do
134
+ @api.transformation("c_scale,w_100")
135
+ @api.delete_transformation("c_scale,w_100")
136
+ lambda{@api.transformation("c_scale,w_100")}.should raise_error(Cloudinary::Api::NotFound)
137
+ end
138
+
139
+ end
data/spec/logo.png ADDED
Binary file
data/spec/utils_spec.rb CHANGED
@@ -104,6 +104,12 @@ describe Cloudinary::Utils do
104
104
  result.should == "http://res.cloudinary.com/test123/image/upload/c_fill,w_200,x_100,y_100/r_10/c_crop,w_100/test"
105
105
  end
106
106
 
107
+ it "should support array of tranformations" do
108
+ options = [{:x=>100, :y=>100, :width=>200, :crop=>:fill}, {:radius=>10}]
109
+ result = Cloudinary::Utils.generate_transformation_string(options)
110
+ result.should == "c_fill,w_200,x_100,y_100/r_10"
111
+ end
112
+
107
113
  it "should not include empty tranformations" do
108
114
  options = {:transformation=>[{}, {:x=>100, :y=>100, :crop=>:fill}, {}]}
109
115
  result = Cloudinary::Utils.cloudinary_url("test", options)
@@ -191,6 +197,11 @@ describe Cloudinary::Utils do
191
197
  result = Cloudinary::Utils.cloudinary_url("test", options)
192
198
  options.should == {}
193
199
  result.should == "http://res.cloudinary.com/test123/image/upload/a_55/test"
200
+
201
+ options = {:angle=>["auto", "55"]}
202
+ result = Cloudinary::Utils.cloudinary_url("test", options)
203
+ options.should == {}
204
+ result.should == "http://res.cloudinary.com/test123/image/upload/a_auto.55/test"
194
205
  end
195
206
 
196
207
  it "should support format for fetch urls" do
@@ -273,4 +284,12 @@ describe Cloudinary::Utils do
273
284
  options.should == {}
274
285
  result.should == "http://a2.hello.com/test123/image/upload/test"
275
286
  end
287
+
288
+ it "should support string param" do
289
+ options = {"effect"=>{"sepia"=>10}}
290
+ result = Cloudinary::Utils.cloudinary_url("test", options)
291
+ options.should == {}
292
+ result.should == "http://res.cloudinary.com/test123/image/upload/e_sepia:10/test"
293
+ end
294
+
276
295
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloudinary
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.32
4
+ version: 1.0.33
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2012-07-26 00:00:00.000000000 Z
14
+ date: 2012-08-05 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: rest-client
@@ -29,6 +29,22 @@ dependencies:
29
29
  - - ! '>='
30
30
  - !ruby/object:Gem::Version
31
31
  version: '0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: aws_cf_signer
34
+ requirement: !ruby/object:Gem::Requirement
35
+ none: false
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
32
48
  - !ruby/object:Gem::Dependency
33
49
  name: rspec
34
50
  requirement: !ruby/object:Gem::Requirement
@@ -61,6 +77,7 @@ files:
61
77
  - Rakefile
62
78
  - cloudinary.gemspec
63
79
  - lib/cloudinary.rb
80
+ - lib/cloudinary/api.rb
64
81
  - lib/cloudinary/blob.rb
65
82
  - lib/cloudinary/carrier_wave.rb
66
83
  - lib/cloudinary/carrier_wave/error.rb
@@ -80,6 +97,8 @@ files:
80
97
  - lib/cloudinary/utils.rb
81
98
  - lib/cloudinary/version.rb
82
99
  - lib/tasks/cloudinary.rake
100
+ - spec/api_spec.rb
101
+ - spec/logo.png
83
102
  - spec/spec_helper.rb
84
103
  - spec/utils_spec.rb
85
104
  - vendor/assets/javascripts/cloudinary/index.js
@@ -113,5 +132,7 @@ signing_key:
113
132
  specification_version: 3
114
133
  summary: Client library for easily using the Cloudinary service
115
134
  test_files:
135
+ - spec/api_spec.rb
136
+ - spec/logo.png
116
137
  - spec/spec_helper.rb
117
138
  - spec/utils_spec.rb