cloudinary 1.18.1 → 1.19.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42964b1e7b2a93986d13479b494110e502f4d1466f179410e22f58995d75ee1f
4
- data.tar.gz: 80eae5018288bedfa0319537850b3e173142d437cf016c83ee3b8ba1f412c5d5
3
+ metadata.gz: 635b7d9336fcde0655fd8b348bda1cdf443a60e123991a1d6197a5a55e9d159b
4
+ data.tar.gz: 8cef3ea547da816bf1125243e04ed72834414f8529a27579e2ba3916571d79bc
5
5
  SHA512:
6
- metadata.gz: 7021d7cd3d1d828660c68d803edc312dc1b7a7a9ac56cb0d9cc6dee894fa47fbb18950f70eb54e1b3e0a20a3f392632f1ef0ac5786f70ded929b2d912f20f3fa
7
- data.tar.gz: 5c67f41050189259c3442500525bb27d54355a8b510b4e5f9f32535a8fa0b2dcfc9fb1102403afbbe3fc88e3c94c91b42fe6028d64dab48d6cc86062c8bd1061
6
+ metadata.gz: a6852e75360ff11e51b3d6be2e8e15f48b06d39ac17b189b6fedd54f5a3041725514db2a799c8d08db3af6f355c9ae21559b3d69015ad8eea54e701554ec3d40
7
+ data.tar.gz: 32997f5c779e28bb3711fea013d0131b922c92cc4e7314ec37d6c6ca45e8e2e765cfaf2979a3f04f7e4b57a5ddd9dc5ddb0b4c2b0afea9c3ff65e428a6005f41
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ 1.19.0 / 2021-03-05
2
+ ==================
3
+
4
+ New functionality and features
5
+ ------------------------------
6
+
7
+ * Add Account Provisioning API
8
+ * Add support for `api_proxy` parameter
9
+ * Add support for `date` parameter in `usage` Admin API
10
+
11
+ Other Changes
12
+ -------------
13
+
14
+ * Fix direct upload of raw files
15
+ * Improve unit testing of add-ons
16
+ * Change test for `eval` upload parameter
17
+ * Bump vulnerable version of rubyzip
18
+ * Fix `cloudinary.gemspec` glob issue
19
+
1
20
 
2
21
  1.18.1 / 2020-09-30
3
22
  ===================
data/cloudinary.gemspec CHANGED
@@ -15,12 +15,17 @@ Gem::Specification.new do |s|
15
15
 
16
16
  s.rubyforge_project = "cloudinary"
17
17
 
18
- s.files = (`git ls-files`.split("\n") - `git ls-files {test,spec,features,samples}/*`.split("\n")) + Dir.glob("vendor/assets/javascripts/*/*") + Dir.glob("vendor/assets/html/*")
18
+ s.files = `git ls-files`.split("\n").select { |f| !f.start_with?("test", "spec", "features", "samples") } + Dir.glob("vendor/assets/javascripts/*/*") + Dir.glob("vendor/assets/html/*")
19
19
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
20
  s.require_paths = ["lib"]
21
21
 
22
22
  s.add_dependency "aws_cf_signer"
23
- s.add_dependency "rest-client"
23
+
24
+ if RUBY_VERSION >= "2.0.0"
25
+ s.add_dependency "rest-client", ">= 2.0.0"
26
+ else
27
+ s.add_dependency "rest-client"
28
+ end
24
29
 
25
30
  s.add_development_dependency "actionpack"
26
31
  s.add_development_dependency "nokogiri"
@@ -39,7 +44,7 @@ Gem::Specification.new do |s|
39
44
  s.add_development_dependency "railties", "<= 4.2.7" if RUBY_VERSION <= "1.9.3"
40
45
  s.add_development_dependency "rspec-rails"
41
46
 
42
- s.add_development_dependency "rubyzip", "<=1.2.0" # support testing Ruby 1.9
47
+ s.add_development_dependency "rubyzip"
43
48
 
44
49
  if RUBY_VERSION <= "2.4.0"
45
50
  s.add_development_dependency "simplecov", "<= 0.17.1" # support testing Ruby 1.9
@@ -72,6 +72,14 @@ module ActiveStorage
72
72
  instrument :url, key: key do |payload|
73
73
  options = {:resource_type => resource_type(nil, key)}.merge(@options.merge(options.symbolize_keys))
74
74
  options[:public_id] = public_id_internal(key)
75
+ # Provide file format for raw files, since js client does not include original file name.
76
+ #
77
+ # When the file is uploaded from the server, the request includes original filename. That allows Cloudinary
78
+ # to identify file extension and append it to the public id of the file (raw files include file extension
79
+ # in their public id, opposed to transformable assets (images/video) that use only basename). When uploading
80
+ # through direct upload (client side js), filename is missing, and that leads to inconsistent/broken URLs.
81
+ # To avoid that, we explicitly pass file format in options.
82
+ options[:format] = ext_for_file(key) if options[:resource_type] == "raw"
75
83
  options[:context] = {active_storage_key: key}
76
84
  options.delete(:file)
77
85
  payload[:url] = api_uri("upload", options)
@@ -174,7 +182,7 @@ module ActiveStorage
174
182
  # @param [string] content_type The content type of the file.
175
183
  #
176
184
  # @return [string] The extension of the filename.
177
- def ext_for_file(key, filename, content_type)
185
+ def ext_for_file(key, filename = nil, content_type = nil)
178
186
  if filename.blank?
179
187
  options = key.respond_to?(:attributes) ? key.attributes : {}
180
188
  filename = ActiveStorage::Filename.new(options[:filename]) if options.has_key?(:filename)
@@ -200,6 +208,8 @@ module ActiveStorage
200
208
  end
201
209
 
202
210
  def content_type_to_resource_type(content_type)
211
+ return 'image' if content_type.nil?
212
+
203
213
  type, subtype = content_type.split('/')
204
214
  case type
205
215
  when 'video', 'audio'
data/lib/cloudinary.rb CHANGED
@@ -16,7 +16,12 @@ require "cloudinary/missing"
16
16
  module Cloudinary
17
17
  autoload :Utils, 'cloudinary/utils'
18
18
  autoload :Uploader, 'cloudinary/uploader'
19
+ autoload :BaseConfig, "cloudinary/base_config"
20
+ autoload :Config, "cloudinary/config"
21
+ autoload :AccountConfig, "cloudinary/account_config"
22
+ autoload :BaseApi, "cloudinary/base_api"
19
23
  autoload :Api, "cloudinary/api"
24
+ autoload :AccountApi, "cloudinary/account_api"
20
25
  autoload :Downloader, "cloudinary/downloader"
21
26
  autoload :Blob, "cloudinary/blob"
22
27
  autoload :PreloadedFile, "cloudinary/preloaded_file"
@@ -58,64 +63,42 @@ module Cloudinary
58
63
  "ept" => "eps"
59
64
  }
60
65
 
61
- @@config = nil
62
-
66
+ # Cloudinary config
67
+ #
68
+ # @param [Hash] new_config If +new_config+ is passed, Config will be updated with it
69
+ # @yieldparam [OpenStruct] Config can be updated in the block
70
+ #
71
+ # @return [OpenStruct]
63
72
  def self.config(new_config=nil)
64
- first_time = @@config.nil?
65
- @@config ||= OpenStruct.new((YAML.load(ERB.new(IO.read(config_dir.join("cloudinary.yml"))).result)[config_env] rescue {}))
66
-
67
- config_from_env if first_time
73
+ @@config ||= make_new_config(Config)
68
74
 
69
- set_config(new_config) if new_config
70
- yield(@@config) if block_given?
75
+ @@config.update(new_config) if new_config
76
+ yield @@config if block_given?
71
77
 
72
78
  @@config
73
79
  end
74
80
 
75
- def self.config_from_url(url)
76
- @@config ||= OpenStruct.new
77
- return unless url && !url.empty?
78
- uri = URI.parse(url)
79
- if !uri.scheme || "cloudinary" != uri.scheme.downcase
80
- raise(CloudinaryException,
81
- "Invalid CLOUDINARY_URL scheme. Expecting to start with 'cloudinary://'")
82
- end
83
- set_config(
84
- "cloud_name" => uri.host,
85
- "api_key" => uri.user,
86
- "api_secret" => uri.password,
87
- "private_cdn" => !uri.path.blank?,
88
- "secure_distribution" => uri.path[1..-1]
89
- )
90
- uri.query.to_s.split("&").each do
91
- |param|
92
- key, value = param.split("=")
93
- if isNestedKey? key
94
- putNestedKey key, value
95
- else
96
- set_config(key => Utils.smart_unescape(value))
97
- end
98
- end
99
- end
81
+ # Cloudinary account config
82
+ #
83
+ # @param [Hash] new_config If +new_config+ is passed, Account Config will be updated with it
84
+ # @yieldparam [OpenStruct] Account config can be updated in the block
85
+ #
86
+ # @return [OpenStruct]
87
+ def self.account_config(new_config=nil)
88
+ @@account_config ||= make_new_config(AccountConfig)
100
89
 
101
- def self.putNestedKey(key, value)
102
- chain = key.split(/[\[\]]+/).reject { |i| i.empty? }
103
- outer = @@config
104
- lastKey = chain.pop()
105
- chain.each do |innerKey|
106
- inner = outer[innerKey]
107
- if inner.nil?
108
- inner = OpenStruct.new
109
- outer[innerKey] = inner
110
- end
111
- outer = inner
112
- end
113
- outer[lastKey] = value
90
+ @@account_config.update(new_config) if new_config
91
+ yield @@account_config if block_given?
92
+
93
+ @@account_config
114
94
  end
115
95
 
96
+ def self.config_from_url(url)
97
+ config.load_from_url(url)
98
+ end
116
99
 
117
- def self.isNestedKey?(key)
118
- /\w+\[\w+\]/ =~ key
100
+ def self.config_from_account_url(url)
101
+ account_config.load_from_url(url)
119
102
  end
120
103
 
121
104
  def self.app_root
@@ -129,22 +112,6 @@ module Cloudinary
129
112
 
130
113
  private
131
114
 
132
- def self.config_from_env
133
- # Heroku support
134
- if ENV["CLOUDINARY_CLOUD_NAME"]
135
- config_keys = ENV.keys.select! { |key| key.start_with? "CLOUDINARY_" }
136
- config_keys -= ["CLOUDINARY_URL"] # ignore it when explicit options are passed
137
- config_keys.each do |full_key|
138
- conf_key = full_key["CLOUDINARY_".length..-1].downcase # convert "CLOUDINARY_CONFIG_NAME" to "config_name"
139
- conf_val = ENV[full_key]
140
- conf_val = conf_val == 'true' if %w[true false].include?(conf_val) # cast relevant boolean values
141
- set_config(conf_key => conf_val)
142
- end
143
- elsif ENV["CLOUDINARY_URL"]
144
- config_from_url(ENV["CLOUDINARY_URL"])
145
- end
146
- end
147
-
148
115
  def self.config_env
149
116
  return ENV["CLOUDINARY_ENV"] if ENV["CLOUDINARY_ENV"]
150
117
  return Rails.env if defined? Rails::env
@@ -159,6 +126,20 @@ module Cloudinary
159
126
  def self.set_config(new_config)
160
127
  new_config.each{|k,v| @@config.send(:"#{k}=", v) if !v.nil?}
161
128
  end
129
+
130
+ # Builds config from yaml file, extends it with specific module and loads configuration from environment variable
131
+ #
132
+ # @param [Module] config_module Config is extended with this module after being built
133
+ #
134
+ # @return [OpenStruct]
135
+ def self.make_new_config(config_module)
136
+ OpenStruct.new((YAML.load(ERB.new(IO.read(config_dir.join("cloudinary.yml"))).result)[config_env] rescue {})).tap do |config|
137
+ config.extend(config_module)
138
+ config.load_config_from_env
139
+ end
140
+ end
141
+
142
+ private_class_method :make_new_config
162
143
  end
163
144
  # Prevent require loop if included after Rails is already initialized.
164
145
  require "cloudinary/helper" if defined?(::ActionView::Base)
@@ -0,0 +1,231 @@
1
+ class Cloudinary::AccountApi
2
+ extend Cloudinary::BaseApi
3
+
4
+ # Creates a new sub-account. Any users that have access to all sub-accounts will also automatically have access to the
5
+ # new sub-account.
6
+ # @param [String] name The display name as shown in the management console
7
+ # @param [String] cloud_name A case-insensitive cloud name comprised of alphanumeric and underscore characters.
8
+ # Generates an error if the specified cloud name is not unique across all Cloudinary accounts.
9
+ # Note: Once created, the name can only be changed for accounts with fewer than 1000 assets.
10
+ # @param [Object] custom_attributes Any custom attributes you want to associate with the sub-account
11
+ # @param [Boolean] enabled Whether to create the account as enabled (default is enabled)
12
+ # @param [String] base_account ID of sub-account from which to copy settings
13
+ # @param [Object] options additional options
14
+ def self.create_sub_account(name, cloud_name = nil, custom_attributes = {}, enabled = nil, base_account = nil, options = {})
15
+ params = {
16
+ name: name,
17
+ cloud_name: cloud_name,
18
+ custom_attributes: custom_attributes,
19
+ enabled: enabled,
20
+ base_sub_account_id: base_account
21
+ }
22
+
23
+ call_account_api(:post, 'sub_accounts', params, options.merge(content_type: :json))
24
+ end
25
+
26
+ # Updates the specified details of the sub-account.
27
+ # @param [String] sub_account_id The ID of the sub-account.
28
+ # @param [String] name The display name as shown in the management console
29
+ # @param [String] cloud_name A case-insensitive cloud name comprised of alphanumeric and underscore characters.
30
+ # Generates an error if the specified cloud name is not unique across all Cloudinary accounts.
31
+ # Note: Once created, the name can only be changed for accounts with fewer than 1000 assets.
32
+ # @param [Object] custom_attributes Any custom attributes you want to associate with the sub-account, as a map/hash
33
+ # of key/value pairs.
34
+ # @param [Boolean] enabled Whether the sub-account is enabled.
35
+ # @param [Object] options additional options
36
+ def self.update_sub_account(sub_account_id, name = nil, cloud_name = nil, custom_attributes = nil, enabled = nil, options = {})
37
+ params = {
38
+ name: name,
39
+ cloud_name: cloud_name,
40
+ custom_attributes: custom_attributes,
41
+ enabled: enabled
42
+ }
43
+
44
+ call_account_api(:put, ['sub_accounts', sub_account_id], params, options.merge(content_type: :json))
45
+ end
46
+
47
+ # Lists sub-accounts.
48
+ # @param [Boolean] enabled Whether to only return enabled sub-accounts (true) or disabled accounts (false).
49
+ # Default: all accounts are returned (both enabled and disabled).
50
+ # @param [Array<String>] ids A list of up to 100 sub-account IDs. When provided, other parameters are ignored.
51
+ # @param [String] prefix Returns accounts where the name begins with the specified case-insensitive string.
52
+ # @param [Object] options additional options
53
+ def self.sub_accounts(enabled = nil, ids = [], prefix = nil, options = {})
54
+ params = {
55
+ enabled: enabled,
56
+ ids: ids,
57
+ prefix: prefix
58
+ }
59
+
60
+ call_account_api(:get, 'sub_accounts', params, options.merge(content_type: :json))
61
+ end
62
+
63
+ # Retrieves the details of the specified sub-account.
64
+ # @param [String] sub_account_id The ID of the sub-account.
65
+ # @param [Object] options additional options
66
+ def self.sub_account(sub_account_id, options = {})
67
+ call_account_api(:get, ['sub_accounts', sub_account_id], {}, options.merge(content_type: :json))
68
+ end
69
+
70
+ # Deletes the specified sub-account. Supported only for accounts with fewer than 1000 assets.
71
+ # @param [String] sub_account_id The ID of the sub-account.
72
+ # @param [Object] options additional options
73
+ def self.delete_sub_account(sub_account_id, options = {})
74
+ call_account_api(:delete, ['sub_accounts', sub_account_id], {}, options)
75
+ end
76
+
77
+ # Creates a new user in the account.
78
+ # @param [String] name The name of the user.
79
+ # @param [String] email A unique email address, which serves as the login name and notification address.
80
+ # @param [String] role The role to assign. Possible values: master_admin, admin, billing, technical_admin, reports,
81
+ # media_library_admin, media_library_user
82
+ # @param [Array<String>] sub_account_ids The list of sub-account IDs that this user can access.
83
+ # Note: This parameter is ignored if the role is specified as master_admin.
84
+ # @param [Object] options additional options
85
+ def self.create_user(name, email, role, sub_account_ids = [], options = {})
86
+ params = {
87
+ name: name,
88
+ email: email,
89
+ role: role,
90
+ sub_account_ids: sub_account_ids
91
+ }
92
+
93
+ call_account_api(:post, 'users', params, options.merge(content_type: :json))
94
+ end
95
+
96
+ # Deletes an existing user.
97
+ # @param [String] user_id The ID of the user to delete.
98
+ # @param [Object] options additional options
99
+ def self.delete_user(user_id, options = {})
100
+ call_account_api(:delete, ['users', user_id], {}, options)
101
+ end
102
+
103
+ # Updates the details of the specified user.
104
+ # @param [String] user_id The ID of the user to update.
105
+ # @param [String] name The name of the user.
106
+ # @param [String] email A unique email address, which serves as the login name and notification address.
107
+ # @param [String] role The role to assign. Possible values: master_admin, admin, billing, technical_admin, reports,
108
+ # media_library_admin, media_library_user
109
+ # @param [Array<String>] sub_account_ids The list of sub-account IDs that this user can access.
110
+ # Note: This parameter is ignored if the role is specified as master_admin.
111
+ # @param [Object] options additional options
112
+ def self.update_user(user_id, name = nil, email = nil, role = nil, sub_account_ids = nil, options = {})
113
+ params = {
114
+ name: name,
115
+ email: email,
116
+ role: role,
117
+ sub_account_ids: sub_account_ids
118
+ }
119
+
120
+ call_account_api(:put, ['users', user_id], params, options.merge(content_type: :json))
121
+ end
122
+
123
+ # Returns the user with the specified ID.
124
+ # @param [String] user_id The ID of the user.
125
+ # @param [Object] options additional options
126
+ def self.user(user_id, options = {})
127
+ call_account_api(:get, ['users', user_id], {}, options.merge(content_type: :json))
128
+ end
129
+
130
+ # Lists users in the account.
131
+ # @param [Boolean] pending Whether to only return pending users. Default: all users
132
+ # @param [Array<String>] user_ids A list of up to 100 user IDs. When provided, other parameters are ignored.
133
+ # @param [String] prefix Returns users where the name or email address begins with the specified case-insensitive string.
134
+ # @param [String] sub_account_id Only returns users who have access to the specified account.
135
+ # @param [Object] options additional options
136
+ def self.users(pending = nil, user_ids = [], prefix = nil, sub_account_id = nil, options = {})
137
+ params = if user_ids && user_ids.count > 0
138
+ {
139
+ ids: user_ids
140
+ }
141
+ else
142
+ {
143
+ prefix: prefix,
144
+ sub_account_id: sub_account_id,
145
+ pending: pending
146
+ }
147
+ end
148
+
149
+ call_account_api(:get, 'users', params, options.merge(content_type: :json))
150
+ end
151
+
152
+ # Creates a new user group.
153
+ # @param [String] name The name for the user group.
154
+ # @param [Object] options additional options
155
+ def self.create_user_group(name, options = {})
156
+ params = {
157
+ name: name
158
+ }
159
+
160
+ call_account_api(:post, 'user_groups', params, options.merge(content_type: :json))
161
+ end
162
+
163
+ # Updates the specified user group.
164
+ # @param [String] group_id The ID of the user group to update.
165
+ # @param [String] name The name for the user group.
166
+ # @param [Object] options additional options
167
+ def self.update_user_group(group_id, name, options = {})
168
+ params = {
169
+ name: name
170
+ }
171
+
172
+ call_account_api(:put, ['user_groups', group_id], params, options.merge(content_type: :json))
173
+ end
174
+
175
+ # Adds a user to a group with the specified ID.
176
+ # @param [String] group_id The ID of the user group.
177
+ # @param [String] user_id The ID of the user.
178
+ # @param [Object] options additional options
179
+ def self.add_user_to_group(group_id, user_id, options = {})
180
+ call_account_api(:post, ['user_groups', group_id, 'users', user_id], {}, options.merge(content_type: :json))
181
+ end
182
+
183
+ # Removes a user from a group with the specified ID.
184
+ # @param [String] group_id The ID of the user group.
185
+ # @param [String] user_id The ID of the user.
186
+ # @param [Object] options additional options
187
+ def self.remove_user_from_group(group_id, user_id, options = {})
188
+ call_account_api(:delete, ['user_groups', group_id, 'users', user_id], {}, options.merge(content_type: :json))
189
+ end
190
+
191
+ # Deletes the user group with the specified ID.
192
+ # @param [String] group_id The ID of the user group to delete.
193
+ # @param [Object] options additional options
194
+ def self.delete_user_group(group_id, options = {})
195
+ call_account_api(:delete, ['user_groups', group_id], {}, options)
196
+ end
197
+
198
+ # Lists user groups in the account.
199
+ # @param [Object] options additional options
200
+ def self.user_groups(options = {})
201
+ call_account_api(:get, 'user_groups', {}, options.merge(content_type: :json))
202
+ end
203
+
204
+ # Retrieves the details of the specified user group.
205
+ # @param [String] group_id The ID of the user group to retrieve.
206
+ # @param [Object] options additional options
207
+ def self.user_group(group_id, options = {})
208
+ call_account_api(:get, ['user_groups', group_id], {}, options.merge(content_type: :json))
209
+ end
210
+
211
+ # Lists users in the specified user group.
212
+ # @param [String] group_id The ID of the user group.
213
+ # @param [Object] options additional options
214
+ def self.user_group_users(group_id, options = {})
215
+ call_account_api(:get, ['user_groups', group_id, 'users'], {}, options.merge(content_type: :json))
216
+ end
217
+
218
+ def self.call_account_api(method, uri, params, options)
219
+ account_id = options[:account_id] || Cloudinary.account_config.account_id || raise('Must supply account_id')
220
+ api_key = options[:provisioning_api_key] || Cloudinary.account_config.provisioning_api_key || raise('Must supply provisioning api_key')
221
+ api_secret = options[:provisioning_api_secret] || Cloudinary.account_config.provisioning_api_secret || raise('Must supply provisioning api_secret')
222
+
223
+ params.reject! { |_, v| v.nil? }
224
+
225
+ call_cloudinary_api(method, uri, api_key, api_secret, params, options) do |cloudinary, inner_uri|
226
+ [cloudinary, 'v1_1', 'provisioning', 'accounts', account_id, inner_uri]
227
+ end
228
+ end
229
+
230
+ private_class_method :call_account_api
231
+ end
@@ -0,0 +1,30 @@
1
+ module Cloudinary
2
+ module AccountConfig
3
+ include BaseConfig
4
+
5
+ ENV_URL = "CLOUDINARY_ACCOUNT_URL"
6
+ SCHEME = "account"
7
+
8
+ def load_config_from_env
9
+ load_from_url(ENV[ENV_URL]) if ENV[ENV_URL]
10
+ end
11
+
12
+ private
13
+
14
+ def env_url
15
+ ENV_URL
16
+ end
17
+
18
+ def expected_scheme
19
+ SCHEME
20
+ end
21
+
22
+ def config_from_parsed_url(parsed_url)
23
+ {
24
+ "account_id" => parsed_url.host,
25
+ "provisioning_api_key" => parsed_url.user,
26
+ "provisioning_api_secret" => parsed_url.password
27
+ }
28
+ end
29
+ end
30
+ end
@@ -1,37 +1,28 @@
1
- require 'rest_client'
2
- require 'json'
3
-
4
1
  class Cloudinary::Api
5
- class Error < CloudinaryException; end
6
- class NotFound < Error; end
7
- class NotAllowed < Error; end
8
- class AlreadyExists < Error; end
9
- class RateLimited < Error; end
10
- class BadRequest < Error; end
11
- class GeneralError < Error; end
12
- class AuthorizationRequired < Error; end
13
-
14
- class Response < Hash
15
- attr_reader :rate_limit_reset_at, :rate_limit_remaining, :rate_limit_allowed
16
-
17
- def initialize(response=nil)
18
- if response
19
- # This sets the instantiated self as the response Hash
20
- update Cloudinary::Api.parse_json_response response
21
-
22
- @rate_limit_allowed = response.headers[:x_featureratelimit_limit].to_i if response.headers[:x_featureratelimit_limit]
23
- @rate_limit_reset_at = Time.parse(response.headers[:x_featureratelimit_reset]) if response.headers[:x_featureratelimit_reset]
24
- @rate_limit_remaining = response.headers[:x_featureratelimit_remaining].to_i if response.headers[:x_featureratelimit_remaining]
25
- end
26
- end
27
- end
2
+ extend Cloudinary::BaseApi
28
3
 
29
4
  def self.ping(options={})
30
5
  call_api(:get, "ping", {}, options)
31
6
  end
32
7
 
8
+ # Gets account usage details
9
+ #
10
+ # Get a report on the status of your Cloudinary account usage details, including
11
+ # storage, bandwidth, requests, number of resources, and add-on usage.
12
+ # Note that numbers are updated periodically.
13
+ #
14
+ # @see https://cloudinary.com/documentation/admin_api#get_account_usage_details Get account usage details
15
+ #
16
+ # @param [Hash] options Additional options
17
+ # @return [Cloudinary::Api::Response]
18
+ # @raise [Cloudinary::Api:Error]
33
19
  def self.usage(options={})
34
- call_api(:get, "usage", {}, options)
20
+ uri = 'usage'
21
+ date = options[:date]
22
+
23
+ uri += "/#{Cloudinary::Utils.to_usage_api_date_format(date)}" unless date.nil?
24
+
25
+ call_api(:get, uri, {}, options)
35
26
  end
36
27
 
37
28
  def self.resource_types(options={})
@@ -489,44 +480,15 @@ class Cloudinary::Api
489
480
  protected
490
481
 
491
482
  def self.call_api(method, uri, params, options)
492
- cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || "https://api.cloudinary.com"
493
- cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise("Must supply cloud_name")
494
- api_key = options[:api_key] || Cloudinary.config.api_key || raise("Must supply api_key")
495
- api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise("Must supply api_secret")
496
- timeout = options[:timeout] || Cloudinary.config.timeout || 60
497
- uri = Cloudinary::Utils.smart_escape(uri)
498
- api_url = [cloudinary, "v1_1", cloud_name, uri].join("/")
499
- # Add authentication
500
- api_url.sub!(%r(^(https?://)), "\\1#{api_key}:#{api_secret}@")
501
-
502
- headers = { "User-Agent" => Cloudinary::USER_AGENT }
503
- if options[:content_type]== :json
504
- payload = params.to_json
505
- headers.merge!("Content-Type"=> 'application/json', "Accept"=> 'application/json')
506
- else
507
- payload = params.reject { |k, v| v.nil? || v=="" }
508
- end
509
- call_json_api(method, api_url, payload, timeout, headers)
510
- end
511
-
512
- def self.call_json_api(method, api_url, payload, timeout, headers)
513
- RestClient::Request.execute(:method => method, :url => api_url, :payload => payload, :timeout => timeout, :headers => headers) do
514
- |response, request, tmpresult|
515
- return Response.new(response) if response.code == 200
516
- exception_class = case response.code
517
- when 400 then BadRequest
518
- when 401 then AuthorizationRequired
519
- when 403 then NotAllowed
520
- when 404 then NotFound
521
- when 409 then AlreadyExists
522
- when 420 then RateLimited
523
- when 500 then GeneralError
524
- else raise GeneralError.new("Server returned unexpected status code - #{response.code} - #{response.body}")
525
- end
526
- json = parse_json_response(response)
527
- raise exception_class.new(json["error"]["message"])
483
+ cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise('Must supply cloud_name')
484
+ api_key = options[:api_key] || Cloudinary.config.api_key || raise('Must supply api_key')
485
+ api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise('Must supply api_secret')
486
+
487
+ call_cloudinary_api(method, uri, api_key, api_secret, params, options) do |cloudinary, inner_uri|
488
+ [cloudinary, 'v1_1', cloud_name, inner_uri]
528
489
  end
529
490
  end
491
+
530
492
  def self.parse_json_response(response)
531
493
  return Cloudinary::Utils.json_decode(response.body)
532
494
  rescue => e
@@ -593,5 +555,4 @@ class Cloudinary::Api
593
555
  params[by_key] = value
594
556
  call_api("post", "resources/#{resource_type}/#{type}/update_access_mode", params, options)
595
557
  end
596
-
597
558
  end
@@ -0,0 +1,79 @@
1
+ require "rest_client"
2
+ require "json"
3
+
4
+ module Cloudinary::BaseApi
5
+ class Error < CloudinaryException; end
6
+ class NotFound < Error; end
7
+ class NotAllowed < Error; end
8
+ class AlreadyExists < Error; end
9
+ class RateLimited < Error; end
10
+ class BadRequest < Error; end
11
+ class GeneralError < Error; end
12
+ class AuthorizationRequired < Error; end
13
+
14
+ class Response < Hash
15
+ attr_reader :rate_limit_reset_at, :rate_limit_remaining, :rate_limit_allowed
16
+
17
+ def initialize(response=nil)
18
+ if response
19
+ # This sets the instantiated self as the response Hash
20
+ update Cloudinary::Api.parse_json_response response
21
+
22
+ @rate_limit_allowed = response.headers[:x_featureratelimit_limit].to_i if response.headers[:x_featureratelimit_limit]
23
+ @rate_limit_reset_at = Time.parse(response.headers[:x_featureratelimit_reset]) if response.headers[:x_featureratelimit_reset]
24
+ @rate_limit_remaining = response.headers[:x_featureratelimit_remaining].to_i if response.headers[:x_featureratelimit_remaining]
25
+ end
26
+ end
27
+ end
28
+
29
+ def self.extended(base)
30
+ [Error, NotFound, NotAllowed, AlreadyExists, RateLimited, BadRequest, GeneralError, AuthorizationRequired, Response].each do |constant|
31
+ base.const_set(constant.name.split("::").last, constant)
32
+ end
33
+ end
34
+
35
+ def call_json_api(method, api_url, payload, timeout, headers, proxy = nil, user = nil, password = nil)
36
+ RestClient::Request.execute(method: method,
37
+ url: api_url,
38
+ payload: payload,
39
+ timeout: timeout,
40
+ headers: headers,
41
+ proxy: proxy,
42
+ user: user,
43
+ password: password) do |response|
44
+ return Response.new(response) if response.code == 200
45
+ exception_class = case response.code
46
+ when 400 then BadRequest
47
+ when 401 then AuthorizationRequired
48
+ when 403 then NotAllowed
49
+ when 404 then NotFound
50
+ when 409 then AlreadyExists
51
+ when 420 then RateLimited
52
+ when 500 then GeneralError
53
+ else raise GeneralError.new("Server returned unexpected status code - #{response.code} - #{response.body}")
54
+ end
55
+ json = Cloudinary::Api.parse_json_response(response)
56
+ raise exception_class.new(json["error"]["message"])
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def call_cloudinary_api(method, uri, user, password, params, options, &api_url_builder)
63
+ cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || 'https://api.cloudinary.com'
64
+ api_url = Cloudinary::Utils.smart_escape(api_url_builder.call(cloudinary, uri).flatten.join('/'))
65
+ timeout = options[:timeout] || Cloudinary.config.timeout || 60
66
+ proxy = options[:api_proxy] || Cloudinary.config.api_proxy
67
+
68
+ headers = { "User-Agent" => Cloudinary::USER_AGENT }
69
+
70
+ if options[:content_type] == :json
71
+ payload = params.to_json
72
+ headers.merge!("Content-Type" => "application/json", "Accept" => "application/json")
73
+ else
74
+ payload = params.reject { |_, v| v.nil? || v == "" }
75
+ end
76
+
77
+ call_json_api(method, api_url, payload, timeout, headers, proxy, user, password)
78
+ end
79
+ end
@@ -0,0 +1,70 @@
1
+ module Cloudinary
2
+ module BaseConfig
3
+ def load_from_url(url)
4
+ return unless url && !url.empty?
5
+
6
+ parsed_url = URI.parse(url)
7
+ scheme = parsed_url.scheme.to_s.downcase
8
+
9
+ if expected_scheme != scheme
10
+ raise(CloudinaryException,
11
+ "Invalid #{env_url} scheme. Expecting to start with '#{expected_scheme}://'")
12
+ end
13
+
14
+ update(config_from_parsed_url(parsed_url))
15
+ setup_from_parsed_url(parsed_url)
16
+ end
17
+
18
+ def update(new_config = {})
19
+ new_config.each{ |k,v| public_send(:"#{k}=", v) unless v.nil?}
20
+ end
21
+
22
+ def load_config_from_env
23
+ raise NotImplementedError
24
+ end
25
+
26
+ private
27
+
28
+ def config_from_parsed_url(parsed_url)
29
+ raise NotImplementedError
30
+ end
31
+
32
+ def env_url
33
+ raise NotImplementedError
34
+ end
35
+
36
+ def expected_scheme
37
+ raise NotImplementedError
38
+ end
39
+
40
+ def put_nested_key(key, value)
41
+ chain = key.split(/[\[\]]+/).reject(&:empty?)
42
+ outer = self
43
+ lastKey = chain.pop
44
+ chain.each do |innerKey|
45
+ inner = outer[innerKey]
46
+ if inner.nil?
47
+ inner = OpenStruct.new
48
+ outer[innerKey] = inner
49
+ end
50
+ outer = inner
51
+ end
52
+ outer[lastKey] = value
53
+ end
54
+
55
+ def is_nested_key?(key)
56
+ /\w+\[\w+\]/ =~ key
57
+ end
58
+
59
+ def setup_from_parsed_url(parsed_url)
60
+ parsed_url.query.to_s.split("&").each do |param|
61
+ key, value = param.split("=")
62
+ if is_nested_key? key
63
+ put_nested_key key, value
64
+ else
65
+ update(key => Utils.smart_unescape(value))
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,43 @@
1
+ module Cloudinary
2
+ module Config
3
+ include BaseConfig
4
+
5
+ ENV_URL = "CLOUDINARY_URL"
6
+ SCHEME = "cloudinary"
7
+
8
+ def load_config_from_env
9
+ if ENV["CLOUDINARY_CLOUD_NAME"]
10
+ config_keys = ENV.keys.select! { |key| key.start_with? "CLOUDINARY_" }
11
+ config_keys -= ["CLOUDINARY_URL"] # ignore it when explicit options are passed
12
+ config_keys.each do |full_key|
13
+ conf_key = full_key["CLOUDINARY_".length..-1].downcase # convert "CLOUDINARY_CONFIG_NAME" to "config_name"
14
+ conf_val = ENV[full_key]
15
+ conf_val = conf_val == 'true' if %w[true false].include?(conf_val) # cast relevant boolean values
16
+ update(conf_key => conf_val)
17
+ end
18
+ elsif ENV[ENV_URL]
19
+ load_from_url(ENV[ENV_URL])
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def env_url
26
+ ENV_URL
27
+ end
28
+
29
+ def expected_scheme
30
+ SCHEME
31
+ end
32
+
33
+ def config_from_parsed_url(parsed_url)
34
+ {
35
+ "cloud_name" => parsed_url.host,
36
+ "api_key" => parsed_url.user,
37
+ "api_secret" => parsed_url.password,
38
+ "private_cdn" => !parsed_url.path.blank?,
39
+ "secure_distribution" => parsed_url.path[1..-1]
40
+ }
41
+ end
42
+ end
43
+ end
@@ -346,6 +346,7 @@ class Cloudinary::Uploader
346
346
  params[:signature] = Cloudinary::Utils.api_sign_request(params.reject { |k, v| non_signable.include?(k) }, api_secret)
347
347
  params[:api_key] = api_key
348
348
  end
349
+ proxy = options[:api_proxy] || Cloudinary.config.api_proxy
349
350
  timeout = options.fetch(:timeout) { Cloudinary.config.to_h.fetch(:timeout, 60) }
350
351
 
351
352
  result = nil
@@ -355,7 +356,7 @@ class Cloudinary::Uploader
355
356
  headers['Content-Range'] = options[:content_range] if options[:content_range]
356
357
  headers['X-Unique-Upload-Id'] = options[:unique_upload_id] if options[:unique_upload_id]
357
358
  headers.merge!(options[:extra_headers]) if options[:extra_headers]
358
- RestClient::Request.execute(:method => :post, :url => api_url, :payload => params.reject { |k, v| v.nil? || v=="" }, :timeout => timeout, :headers => headers) do
359
+ RestClient::Request.execute(:method => :post, :url => api_url, :payload => params.reject { |k, v| v.nil? || v=="" }, :timeout => timeout, :headers => headers, :proxy => proxy) do
359
360
  |response, request, tmpresult|
360
361
  raise CloudinaryException, "Server returned unexpected status code - #{response.code} - #{response.body}" unless [200, 400, 401, 403, 404, 500].include?(response.code)
361
362
  begin
@@ -1149,6 +1149,20 @@ class Cloudinary::Utils
1149
1149
  REMOTE_URL_REGEX === url
1150
1150
  end
1151
1151
 
1152
+ # Format date in a format accepted by the usage API (e.g., 31-12-2020) if
1153
+ # passed value is of type Date, otherwise return the string representation of
1154
+ # the input.
1155
+ #
1156
+ # @param [Date|Object] date
1157
+ # @return [String]
1158
+ def self.to_usage_api_date_format(date)
1159
+ if date.is_a?(Date)
1160
+ date.strftime('%d-%m-%Y')
1161
+ else
1162
+ date.to_s
1163
+ end
1164
+ end
1165
+
1152
1166
  # Computes a short or long signature based on a message and secret
1153
1167
  # @param [String] message The string to sign
1154
1168
  # @param [String] secret A secret that will be added to the message when signing
@@ -1,4 +1,4 @@
1
1
  # Copyright Cloudinary
2
2
  module Cloudinary
3
- VERSION = "1.18.1"
3
+ VERSION = "1.19.0"
4
4
  end
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloudinary
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.18.1
4
+ version: 1.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nadav Soferman
8
8
  - Itai Lahan
9
9
  - Tal Lev-Ami
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-09-30 00:00:00.000000000 Z
13
+ date: 2021-03-05 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: aws_cf_signer
@@ -32,14 +32,14 @@ dependencies:
32
32
  requirements:
33
33
  - - ">="
34
34
  - !ruby/object:Gem::Version
35
- version: '0'
35
+ version: 2.0.0
36
36
  type: :runtime
37
37
  prerelease: false
38
38
  version_requirements: !ruby/object:Gem::Requirement
39
39
  requirements:
40
40
  - - ">="
41
41
  - !ruby/object:Gem::Version
42
- version: '0'
42
+ version: 2.0.0
43
43
  - !ruby/object:Gem::Dependency
44
44
  name: actionpack
45
45
  requirement: !ruby/object:Gem::Requirement
@@ -156,16 +156,16 @@ dependencies:
156
156
  name: rubyzip
157
157
  requirement: !ruby/object:Gem::Requirement
158
158
  requirements:
159
- - - "<="
159
+ - - ">="
160
160
  - !ruby/object:Gem::Version
161
- version: 1.2.0
161
+ version: '0'
162
162
  type: :development
163
163
  prerelease: false
164
164
  version_requirements: !ruby/object:Gem::Requirement
165
165
  requirements:
166
- - - "<="
166
+ - - ">="
167
167
  - !ruby/object:Gem::Version
168
- version: 1.2.0
168
+ version: '0'
169
169
  - !ruby/object:Gem::Dependency
170
170
  name: simplecov
171
171
  requirement: !ruby/object:Gem::Requirement
@@ -204,8 +204,12 @@ files:
204
204
  - lib/active_storage/blob_key.rb
205
205
  - lib/active_storage/service/cloudinary_service.rb
206
206
  - lib/cloudinary.rb
207
+ - lib/cloudinary/account_api.rb
208
+ - lib/cloudinary/account_config.rb
207
209
  - lib/cloudinary/api.rb
208
210
  - lib/cloudinary/auth_token.rb
211
+ - lib/cloudinary/base_api.rb
212
+ - lib/cloudinary/base_config.rb
209
213
  - lib/cloudinary/blob.rb
210
214
  - lib/cloudinary/cache.rb
211
215
  - lib/cloudinary/cache/breakpoints_cache.rb
@@ -219,6 +223,7 @@ files:
219
223
  - lib/cloudinary/carrier_wave/remote.rb
220
224
  - lib/cloudinary/carrier_wave/storage.rb
221
225
  - lib/cloudinary/cloudinary_controller.rb
226
+ - lib/cloudinary/config.rb
222
227
  - lib/cloudinary/downloader.rb
223
228
  - lib/cloudinary/engine.rb
224
229
  - lib/cloudinary/exceptions.rb
@@ -256,7 +261,7 @@ homepage: http://cloudinary.com
256
261
  licenses:
257
262
  - MIT
258
263
  metadata: {}
259
- post_install_message:
264
+ post_install_message:
260
265
  rdoc_options: []
261
266
  require_paths:
262
267
  - lib
@@ -271,8 +276,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
271
276
  - !ruby/object:Gem::Version
272
277
  version: '0'
273
278
  requirements: []
274
- rubygems_version: 3.1.4
275
- signing_key:
279
+ rubygems_version: 3.1.2
280
+ signing_key:
276
281
  specification_version: 4
277
282
  summary: Client library for easily using the Cloudinary service
278
283
  test_files: []