cloudinary 1.0.32 → 1.0.33

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