uploadcare-ruby 4.4.2 → 5.0.0.rc1
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.
- checksums.yaml +4 -4
- data/.env.example +7 -0
- data/.github/workflows/gem-push.yml +1 -1
- data/.github/workflows/ruby.yml +10 -13
- data/.gitignore +9 -0
- data/.rubocop.yml +95 -8
- data/CHANGELOG.md +78 -0
- data/Gemfile +23 -6
- data/MIGRATING_V5.md +290 -0
- data/README.md +422 -671
- data/Rakefile +5 -1
- data/api_examples/README.md +77 -0
- data/api_examples/rest_api/delete_files_storage.rb +3 -5
- data/api_examples/rest_api/delete_files_uuid_metadata_key.rb +3 -4
- data/api_examples/rest_api/delete_files_uuid_storage.rb +3 -4
- data/api_examples/rest_api/delete_groups_uuid.rb +3 -4
- data/api_examples/rest_api/delete_webhooks_unsubscribe.rb +3 -4
- data/api_examples/rest_api/get_addons_aws_rekognition_detect_labels_execute_status.rb +3 -6
- data/api_examples/rest_api/get_addons_aws_rekognition_detect_moderation_labels_execute_status.rb +3 -6
- data/api_examples/rest_api/get_addons_remove_bg_execute_status.rb +3 -6
- data/api_examples/rest_api/get_addons_uc_clamav_virus_scan_execute_status.rb +3 -6
- data/api_examples/rest_api/get_convert_document_status_token.rb +3 -5
- data/api_examples/rest_api/get_convert_document_uuid.rb +3 -5
- data/api_examples/rest_api/get_convert_video_status_token.rb +3 -5
- data/api_examples/rest_api/get_files.rb +3 -5
- data/api_examples/rest_api/get_files_uuid.rb +3 -5
- data/api_examples/rest_api/get_files_uuid_metadata.rb +3 -5
- data/api_examples/rest_api/get_files_uuid_metadata_key.rb +3 -5
- data/api_examples/rest_api/get_groups.rb +3 -5
- data/api_examples/rest_api/get_groups_uuid.rb +3 -5
- data/api_examples/rest_api/get_project.rb +3 -5
- data/api_examples/rest_api/get_webhooks.rb +3 -5
- data/api_examples/rest_api/post_addons_aws_rekognition_detect_labels_execute.rb +3 -5
- data/api_examples/rest_api/post_addons_aws_rekognition_detect_moderation_labels_execute.rb +3 -5
- data/api_examples/rest_api/post_addons_remove_bg_execute.rb +3 -5
- data/api_examples/rest_api/post_addons_uc_clamav_virus_scan_execute.rb +3 -5
- data/api_examples/rest_api/post_convert_document.rb +3 -6
- data/api_examples/rest_api/post_convert_video.rb +3 -10
- data/api_examples/rest_api/post_files_local_copy.rb +3 -6
- data/api_examples/rest_api/post_files_remote_copy.rb +3 -7
- data/api_examples/rest_api/post_webhooks.rb +3 -9
- data/api_examples/rest_api/put_files_storage.rb +3 -8
- data/api_examples/rest_api/put_files_uuid_metadata_key.rb +3 -7
- data/api_examples/rest_api/put_files_uuid_storage.rb +3 -5
- data/api_examples/rest_api/put_webhooks_id.rb +3 -11
- data/api_examples/support/example_helper.rb +250 -0
- data/api_examples/support/run_rest_example.rb +161 -0
- data/api_examples/support/run_upload_example.rb +88 -0
- data/api_examples/upload_api/get_from_url_status.rb +3 -5
- data/api_examples/upload_api/get_group_info.rb +3 -6
- data/api_examples/upload_api/get_info.rb +3 -6
- data/api_examples/upload_api/post_base.rb +3 -5
- data/api_examples/upload_api/post_from_url.rb +3 -5
- data/api_examples/upload_api/post_group.rb +3 -8
- data/api_examples/upload_api/post_multipart_complete.rb +3 -7
- data/api_examples/upload_api/post_multipart_start.rb +3 -7
- data/api_examples/upload_api/put_multipart_part.rb +4 -0
- data/bin/console +1 -1
- data/docs/release-notes-5.0.0.rc1.md +34 -0
- data/examples/README.md +39 -0
- data/examples/batch_upload.rb +54 -0
- data/examples/group_creation.rb +88 -0
- data/examples/large_file_upload.rb +88 -0
- data/examples/simple_upload.rb +39 -0
- data/examples/upload_with_progress.rb +84 -0
- data/examples/url_upload.rb +56 -0
- data/lib/uploadcare/api/rest/addons.rb +107 -0
- data/lib/uploadcare/api/rest/document_conversions.rb +65 -0
- data/lib/uploadcare/api/rest/file_metadata.rb +71 -0
- data/lib/uploadcare/api/rest/files.rb +112 -0
- data/lib/uploadcare/api/rest/groups.rb +49 -0
- data/lib/uploadcare/api/rest/project.rb +23 -0
- data/lib/uploadcare/api/rest/video_conversions.rb +52 -0
- data/lib/uploadcare/api/rest/webhooks.rb +74 -0
- data/lib/uploadcare/api/rest.rb +254 -0
- data/lib/uploadcare/api/upload/files.rb +313 -0
- data/lib/uploadcare/api/upload/groups.rb +72 -0
- data/lib/uploadcare/api/upload.rb +272 -0
- data/lib/uploadcare/client/addons_accessor.rb +85 -0
- data/lib/uploadcare/client/api.rb +33 -0
- data/lib/uploadcare/client/conversions_accessor.rb +33 -0
- data/lib/uploadcare/client/document_conversions_accessor.rb +41 -0
- data/lib/uploadcare/client/file_metadata_accessor.rb +46 -0
- data/lib/uploadcare/client/files_accessor.rb +82 -0
- data/lib/uploadcare/client/groups_accessor.rb +35 -0
- data/lib/uploadcare/client/project_accessor.rb +17 -0
- data/lib/uploadcare/client/video_conversions_accessor.rb +33 -0
- data/lib/uploadcare/client/webhooks_accessor.rb +42 -0
- data/lib/uploadcare/client.rb +127 -0
- data/lib/uploadcare/cname_generator.rb +68 -0
- data/lib/uploadcare/collections/batch_result.rb +35 -0
- data/lib/uploadcare/collections/paginated.rb +165 -0
- data/lib/uploadcare/configuration.rb +81 -0
- data/lib/uploadcare/exception/auth_error.rb +2 -6
- data/lib/uploadcare/exception/configuration_error.rb +4 -0
- data/lib/uploadcare/exception/conversion_error.rb +2 -6
- data/lib/uploadcare/exception/invalid_request_error.rb +4 -0
- data/lib/uploadcare/exception/multipart_upload_error.rb +4 -0
- data/lib/uploadcare/exception/not_found_error.rb +4 -0
- data/lib/uploadcare/exception/request_error.rb +2 -6
- data/lib/uploadcare/exception/retry_error.rb +2 -6
- data/lib/uploadcare/exception/throttle_error.rb +7 -11
- data/lib/uploadcare/exception/unknown_status_error.rb +4 -0
- data/lib/uploadcare/exception/upload_error.rb +4 -0
- data/lib/uploadcare/exception/upload_timeout_error.rb +4 -0
- data/lib/uploadcare/internal/authenticator.rb +101 -0
- data/lib/uploadcare/internal/error_handler.rb +102 -0
- data/lib/uploadcare/internal/signature_generator.rb +31 -0
- data/lib/uploadcare/internal/throttle_handler.rb +36 -0
- data/lib/uploadcare/internal/upload_io.rb +110 -0
- data/lib/uploadcare/internal/upload_params_generator.rb +86 -0
- data/lib/uploadcare/internal/user_agent.rb +22 -0
- data/lib/uploadcare/operations/multipart_upload.rb +213 -0
- data/lib/uploadcare/operations/upload_router.rb +162 -0
- data/lib/uploadcare/resources/addon_execution.rb +97 -0
- data/lib/uploadcare/resources/base_resource.rb +61 -0
- data/lib/uploadcare/resources/document_conversion.rb +81 -0
- data/lib/uploadcare/resources/file.rb +366 -0
- data/lib/uploadcare/resources/file_metadata.rb +135 -0
- data/lib/uploadcare/resources/group.rb +142 -0
- data/lib/uploadcare/resources/project.rb +26 -0
- data/lib/uploadcare/resources/video_conversion.rb +59 -0
- data/lib/uploadcare/resources/webhook.rb +85 -0
- data/lib/uploadcare/result.rb +85 -0
- data/lib/uploadcare/signed_url_generators/akamai_generator.rb +60 -56
- data/lib/uploadcare/signed_url_generators/base_generator.rb +15 -15
- data/lib/uploadcare/version.rb +7 -0
- data/lib/uploadcare/webhook_signature_verifier.rb +60 -0
- data/lib/uploadcare.rb +84 -50
- data/mise.toml +2 -0
- data/uploadcare-ruby.gemspec +8 -7
- metadata +102 -74
- data/api_examples/upload_api/put_presigned_url_x.rb +0 -8
- data/lib/uploadcare/api/api.rb +0 -25
- data/lib/uploadcare/client/addons_client.rb +0 -69
- data/lib/uploadcare/client/conversion/base_conversion_client.rb +0 -59
- data/lib/uploadcare/client/conversion/document_conversion_client.rb +0 -45
- data/lib/uploadcare/client/conversion/video_conversion_client.rb +0 -46
- data/lib/uploadcare/client/file_client.rb +0 -48
- data/lib/uploadcare/client/file_list_client.rb +0 -46
- data/lib/uploadcare/client/file_metadata_client.rb +0 -36
- data/lib/uploadcare/client/group_client.rb +0 -45
- data/lib/uploadcare/client/multipart_upload/chunks_client.rb +0 -58
- data/lib/uploadcare/client/multipart_upload_client.rb +0 -64
- data/lib/uploadcare/client/project_client.rb +0 -20
- data/lib/uploadcare/client/rest_client.rb +0 -77
- data/lib/uploadcare/client/rest_group_client.rb +0 -43
- data/lib/uploadcare/client/upload_client.rb +0 -46
- data/lib/uploadcare/client/uploader_client.rb +0 -128
- data/lib/uploadcare/client/webhook_client.rb +0 -49
- data/lib/uploadcare/concern/error_handler.rb +0 -54
- data/lib/uploadcare/concern/throttle_handler.rb +0 -25
- data/lib/uploadcare/concern/upload_error_handler.rb +0 -32
- data/lib/uploadcare/entity/addons.rb +0 -14
- data/lib/uploadcare/entity/conversion/base_converter.rb +0 -43
- data/lib/uploadcare/entity/conversion/document_converter.rb +0 -15
- data/lib/uploadcare/entity/conversion/video_converter.rb +0 -15
- data/lib/uploadcare/entity/decorator/paginator.rb +0 -79
- data/lib/uploadcare/entity/entity.rb +0 -18
- data/lib/uploadcare/entity/file.rb +0 -103
- data/lib/uploadcare/entity/file_list.rb +0 -32
- data/lib/uploadcare/entity/file_metadata.rb +0 -30
- data/lib/uploadcare/entity/group.rb +0 -49
- data/lib/uploadcare/entity/group_list.rb +0 -24
- data/lib/uploadcare/entity/project.rb +0 -13
- data/lib/uploadcare/entity/uploader.rb +0 -93
- data/lib/uploadcare/entity/webhook.rb +0 -14
- data/lib/uploadcare/param/authentication_header.rb +0 -37
- data/lib/uploadcare/param/conversion/document/processing_job_url_builder.rb +0 -39
- data/lib/uploadcare/param/conversion/video/processing_job_url_builder.rb +0 -64
- data/lib/uploadcare/param/param.rb +0 -10
- data/lib/uploadcare/param/secure_auth_header.rb +0 -51
- data/lib/uploadcare/param/simple_auth_header.rb +0 -14
- data/lib/uploadcare/param/upload/signature_generator.rb +0 -24
- data/lib/uploadcare/param/upload/upload_params_generator.rb +0 -41
- data/lib/uploadcare/param/user_agent.rb +0 -21
- data/lib/uploadcare/param/webhook_signature_verifier.rb +0 -23
- data/lib/uploadcare/ruby/version.rb +0 -5
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
# Paginated collection for API list responses.
|
|
6
|
+
#
|
|
7
|
+
# Wraps paginated API responses and provides methods for navigating between pages.
|
|
8
|
+
# Implements Enumerable for easy iteration over resources.
|
|
9
|
+
#
|
|
10
|
+
# @example Iterating over resources
|
|
11
|
+
# files = client.files.list
|
|
12
|
+
# files.each { |file| puts file.uuid }
|
|
13
|
+
#
|
|
14
|
+
# @example Navigating pages
|
|
15
|
+
# files = client.files.list
|
|
16
|
+
# while files
|
|
17
|
+
# files.each { |file| process(file) }
|
|
18
|
+
# files = files.next_page
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @example Fetching all resources
|
|
22
|
+
# all_files = client.files.list.all
|
|
23
|
+
class Uploadcare::Collections::Paginated
|
|
24
|
+
include Enumerable
|
|
25
|
+
|
|
26
|
+
# @return [Array] Array of resource objects in the current page
|
|
27
|
+
attr_reader :resources
|
|
28
|
+
|
|
29
|
+
# @return [String, nil] URL for the next page, or nil if on last page
|
|
30
|
+
attr_reader :next_page_url
|
|
31
|
+
|
|
32
|
+
# @return [String, nil] URL for the previous page, or nil if on first page
|
|
33
|
+
attr_reader :previous_page_url
|
|
34
|
+
|
|
35
|
+
# @return [Integer] Number of items per page
|
|
36
|
+
attr_reader :per_page
|
|
37
|
+
|
|
38
|
+
# @return [Integer] Total number of items across all pages
|
|
39
|
+
attr_reader :total
|
|
40
|
+
|
|
41
|
+
# @return [Object] API endpoint client for fetching additional pages
|
|
42
|
+
attr_reader :api_client
|
|
43
|
+
|
|
44
|
+
# @return [Class] Resource class for instantiating items from raw data
|
|
45
|
+
attr_reader :resource_class
|
|
46
|
+
|
|
47
|
+
# @return [Uploadcare::Client, nil] Client for resource instantiation
|
|
48
|
+
attr_reader :client
|
|
49
|
+
|
|
50
|
+
# @return [Hash] Request options used when fetching pages
|
|
51
|
+
attr_reader :request_options
|
|
52
|
+
|
|
53
|
+
# Initialize a new Paginated collection.
|
|
54
|
+
#
|
|
55
|
+
# @param params [Hash] Collection parameters
|
|
56
|
+
# @option params [Array] :resources Array of resource objects
|
|
57
|
+
# @option params [String, nil] :next_page URL for next page
|
|
58
|
+
# @option params [String, nil] :previous_page URL for previous page
|
|
59
|
+
# @option params [Integer] :per_page Items per page
|
|
60
|
+
# @option params [Integer] :total Total item count
|
|
61
|
+
# @option params [Object] :api_client API client for fetching pages
|
|
62
|
+
# @option params [Class] :resource_class Class for instantiating resources
|
|
63
|
+
# @option params [Uploadcare::Client, nil] :client Client for resources
|
|
64
|
+
# @option params [Hash] :request_options Request options for subsequent page fetches
|
|
65
|
+
def initialize(params = {})
|
|
66
|
+
@resources = params[:resources] || []
|
|
67
|
+
@next_page_url = params[:next_page]
|
|
68
|
+
@previous_page_url = params[:previous_page]
|
|
69
|
+
@per_page = params[:per_page]
|
|
70
|
+
@total = params[:total]
|
|
71
|
+
@api_client = params[:api_client]
|
|
72
|
+
@resource_class = params[:resource_class]
|
|
73
|
+
@client = params[:client]
|
|
74
|
+
@request_options = params[:request_options] || {}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Iterate over resources in the current page.
|
|
78
|
+
#
|
|
79
|
+
# @yield [resource] Block to execute for each resource
|
|
80
|
+
# @yieldparam resource [Object] A resource object
|
|
81
|
+
def each(&)
|
|
82
|
+
@resources.each(&)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Fetch the next page of resources.
|
|
86
|
+
#
|
|
87
|
+
# @return [Uploadcare::Collections::Paginated, nil] Next page, or nil if on last page
|
|
88
|
+
def next_page
|
|
89
|
+
fetch_page(@next_page_url)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Fetch the previous page of resources.
|
|
93
|
+
#
|
|
94
|
+
# @return [Uploadcare::Collections::Paginated, nil] Previous page, or nil if on first page
|
|
95
|
+
def previous_page
|
|
96
|
+
fetch_page(@previous_page_url)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Fetch all resources across all pages.
|
|
100
|
+
#
|
|
101
|
+
# @param limit [Integer, nil] Maximum number of resources to return
|
|
102
|
+
# @return [Array<Object>] All resources up to the requested limit
|
|
103
|
+
def all(limit: nil)
|
|
104
|
+
return [] if limit && limit <= 0
|
|
105
|
+
|
|
106
|
+
collection = self
|
|
107
|
+
items = []
|
|
108
|
+
remaining = limit
|
|
109
|
+
|
|
110
|
+
while collection
|
|
111
|
+
page_items = collection.resources || []
|
|
112
|
+
if remaining
|
|
113
|
+
page_slice = page_items.first(remaining)
|
|
114
|
+
items.concat(page_slice)
|
|
115
|
+
remaining -= page_slice.length
|
|
116
|
+
break if remaining <= 0
|
|
117
|
+
else
|
|
118
|
+
items.concat(page_items)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
collection = collection.next_page
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
items
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def fetch_page(page_url)
|
|
130
|
+
return nil unless page_url
|
|
131
|
+
|
|
132
|
+
params = extract_params_from_url(page_url)
|
|
133
|
+
response = fetch_response(params)
|
|
134
|
+
build_paginated_collection(response)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def extract_params_from_url(page_url)
|
|
138
|
+
uri = URI.parse(page_url)
|
|
139
|
+
URI.decode_www_form(uri.query.to_s).to_h
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def fetch_response(params)
|
|
143
|
+
Uploadcare::Result.unwrap(api_client.list(params: params, request_options: request_options))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def build_paginated_collection(response)
|
|
147
|
+
new_resources = build_resources(response['results'])
|
|
148
|
+
|
|
149
|
+
self.class.new(
|
|
150
|
+
resources: new_resources,
|
|
151
|
+
next_page: response['next'],
|
|
152
|
+
previous_page: response['previous'],
|
|
153
|
+
per_page: response['per_page'],
|
|
154
|
+
total: response['total'],
|
|
155
|
+
api_client: api_client,
|
|
156
|
+
resource_class: resource_class,
|
|
157
|
+
client: client,
|
|
158
|
+
request_options: request_options
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def build_resources(results)
|
|
163
|
+
results.map { |data| resource_class.new(data, client) }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
# Configuration container for client defaults and per-client overrides.
|
|
6
|
+
class Uploadcare::Configuration
|
|
7
|
+
attr_accessor :public_key, :secret_key, :auth_type, :multipart_size_threshold, :rest_api_root,
|
|
8
|
+
:upload_api_root, :max_request_tries, :base_request_sleep, :max_request_sleep, :sign_uploads,
|
|
9
|
+
:upload_signature_lifetime, :max_throttle_attempts, :upload_threads, :framework_data,
|
|
10
|
+
:file_chunk_size, :logger, :use_subdomains, :cdn_base_postfix, :default_cdn_base,
|
|
11
|
+
:multipart_chunk_size, :upload_timeout, :max_upload_retries
|
|
12
|
+
|
|
13
|
+
# Default configuration values used as the template for new instances.
|
|
14
|
+
DEFAULTS = {
|
|
15
|
+
public_key: nil,
|
|
16
|
+
secret_key: nil,
|
|
17
|
+
auth_type: 'Uploadcare',
|
|
18
|
+
multipart_size_threshold: 100 * 1024 * 1024,
|
|
19
|
+
rest_api_root: 'https://api.uploadcare.com',
|
|
20
|
+
upload_api_root: 'https://upload.uploadcare.com',
|
|
21
|
+
max_request_tries: 100,
|
|
22
|
+
base_request_sleep: 1, # seconds
|
|
23
|
+
max_request_sleep: 60.0, # seconds
|
|
24
|
+
sign_uploads: false,
|
|
25
|
+
upload_signature_lifetime: 30 * 60, # seconds
|
|
26
|
+
max_throttle_attempts: 5,
|
|
27
|
+
upload_threads: 2, # used for multiupload only ATM
|
|
28
|
+
framework_data: '',
|
|
29
|
+
file_chunk_size: 100,
|
|
30
|
+
logger: nil,
|
|
31
|
+
use_subdomains: false,
|
|
32
|
+
cdn_base_postfix: 'https://ucarecd.net/',
|
|
33
|
+
default_cdn_base: 'https://ucarecdn.com/',
|
|
34
|
+
multipart_chunk_size: 5 * 1024 * 1024, # 5MB chunks for multipart upload
|
|
35
|
+
upload_timeout: 60, # seconds
|
|
36
|
+
max_upload_retries: 3 # retry failed uploads 3 times
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# @param options [Hash] Configuration overrides
|
|
40
|
+
def initialize(**options)
|
|
41
|
+
values = DEFAULTS.merge(options)
|
|
42
|
+
values[:public_key] = ENV.fetch('UPLOADCARE_PUBLIC_KEY', '') unless options.key?(:public_key)
|
|
43
|
+
values[:secret_key] = ENV.fetch('UPLOADCARE_SECRET_KEY', '') unless options.key?(:secret_key)
|
|
44
|
+
|
|
45
|
+
values.each do |attribute, value|
|
|
46
|
+
send("#{attribute}=", value)
|
|
47
|
+
end
|
|
48
|
+
@logger ||= Logger.new($stdout)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Build the deterministic subdomain prefix for the configured public key.
|
|
52
|
+
#
|
|
53
|
+
# @return [String]
|
|
54
|
+
def custom_cname
|
|
55
|
+
Uploadcare::CnameGenerator.generate_cname(public_key: public_key)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Resolve the CDN base URL for this configuration.
|
|
59
|
+
#
|
|
60
|
+
# @return [String]
|
|
61
|
+
def cdn_base
|
|
62
|
+
return Uploadcare::CnameGenerator.cdn_base_postfix(config: self) if use_subdomains
|
|
63
|
+
|
|
64
|
+
default_cdn_base
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Clone this configuration with overrides.
|
|
68
|
+
#
|
|
69
|
+
# @param options [Hash]
|
|
70
|
+
# @return [Uploadcare::Configuration]
|
|
71
|
+
def with(**options)
|
|
72
|
+
self.class.new(**to_h, **options)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Convert this configuration to a serializable hash.
|
|
76
|
+
#
|
|
77
|
+
# @return [Hash]
|
|
78
|
+
def to_h
|
|
79
|
+
DEFAULTS.keys.to_h { |attribute| [attribute, public_send(attribute)] }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# Standard error for invalid API conversion responses
|
|
6
|
-
class ConversionError < StandardError; end
|
|
7
|
-
end
|
|
8
|
-
end
|
|
3
|
+
# Standard error for invalid API conversion responses
|
|
4
|
+
class Uploadcare::Exception::ConversionError < StandardError; end
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# Standard error for invalid API responses
|
|
6
|
-
class RequestError < StandardError; end
|
|
7
|
-
end
|
|
8
|
-
end
|
|
3
|
+
# Standard error for invalid API responses
|
|
4
|
+
class Uploadcare::Exception::RequestError < StandardError; end
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# Standard error to raise when needing to retry a request
|
|
6
|
-
class RetryError < StandardError; end
|
|
7
|
-
end
|
|
8
|
-
end
|
|
3
|
+
# Standard error to raise when needing to retry a request
|
|
4
|
+
class Uploadcare::Exception::RetryError < StandardError; end
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class ThrottleError < StandardError
|
|
7
|
-
attr_reader :timeout
|
|
3
|
+
# Exception for throttled requests
|
|
4
|
+
class Uploadcare::Exception::ThrottleError < StandardError
|
|
5
|
+
attr_reader :timeout
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
end
|
|
14
|
-
end
|
|
7
|
+
# @param timeout [Float] Amount of seconds the request have been throttled for
|
|
8
|
+
def initialize(message = nil, timeout: 10.0)
|
|
9
|
+
super(message)
|
|
10
|
+
@timeout = timeout
|
|
15
11
|
end
|
|
16
12
|
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
# Handles authentication for Uploadcare REST API requests.
|
|
7
|
+
#
|
|
8
|
+
# Supports two authentication modes:
|
|
9
|
+
# - Simple authentication: Basic auth with public_key:secret_key
|
|
10
|
+
# - Secure authentication: signature-based authentication
|
|
11
|
+
#
|
|
12
|
+
# @example Using the authenticator
|
|
13
|
+
# authenticator = Uploadcare::Internal::Authenticator.new(config: config)
|
|
14
|
+
# headers = authenticator.headers('GET', '/files/', '')
|
|
15
|
+
#
|
|
16
|
+
# @see https://uploadcare.com/docs/api_reference/rest/requests_auth/
|
|
17
|
+
class Uploadcare::Internal::Authenticator
|
|
18
|
+
BODY_DIGEST_NAME = 'MD5'
|
|
19
|
+
SIGNATURE_DIGEST_NAME = 'SHA1'
|
|
20
|
+
|
|
21
|
+
# @return [Hash] Default headers included in all requests
|
|
22
|
+
attr_reader :default_headers
|
|
23
|
+
|
|
24
|
+
# Initialize a new Authenticator.
|
|
25
|
+
#
|
|
26
|
+
# @param config [Uploadcare::Configuration] Configuration object with API credentials
|
|
27
|
+
def initialize(config:)
|
|
28
|
+
@config = config
|
|
29
|
+
@default_headers = {
|
|
30
|
+
'Accept' => 'application/vnd.uploadcare-v0.7+json',
|
|
31
|
+
'User-Agent' => Uploadcare::Internal::UserAgent.call(config: config)
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Generate authentication headers for an API request.
|
|
36
|
+
#
|
|
37
|
+
# @param http_method [String] HTTP method (GET, POST, PUT, DELETE)
|
|
38
|
+
# @param uri [String] Request URI path
|
|
39
|
+
# @param body [String] Request body content (default: '')
|
|
40
|
+
# @param content_type [String] Content-Type header value (default: 'application/json')
|
|
41
|
+
# @return [Hash] Headers hash including authentication
|
|
42
|
+
# @raise [Uploadcare::Exception::AuthError] if credentials are blank when using secure auth
|
|
43
|
+
def headers(http_method, uri, body = '', content_type = nil)
|
|
44
|
+
resolved_content_type = content_type || 'application/json'
|
|
45
|
+
raise Uploadcare::Exception::AuthError, 'Secret Key is blank.' if @config.secret_key.to_s.empty?
|
|
46
|
+
|
|
47
|
+
validate_public_key
|
|
48
|
+
|
|
49
|
+
return simple_auth_headers(resolved_content_type) if @config.auth_type == 'Uploadcare.Simple'
|
|
50
|
+
|
|
51
|
+
secure_auth_headers(http_method, uri, body, resolved_content_type)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def simple_auth_headers(content_type)
|
|
57
|
+
@default_headers.merge(
|
|
58
|
+
'Content-Type' => content_type,
|
|
59
|
+
'Authorization' => "#{@config.auth_type} #{@config.public_key}:#{@config.secret_key}"
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def validate_public_key
|
|
64
|
+
return unless @config.public_key.nil? || @config.public_key.empty?
|
|
65
|
+
|
|
66
|
+
raise Uploadcare::Exception::AuthError, 'Public Key is blank.'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def secure_auth_headers(http_method, uri, body, content_type)
|
|
70
|
+
date = Time.now.gmtime.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
|
71
|
+
signature = generate_signature(http_method, uri, body, content_type, date)
|
|
72
|
+
auth_headers = { 'Authorization' => "Uploadcare #{@config.public_key}:#{signature}", 'Date' => date }
|
|
73
|
+
@default_headers.merge('Content-Type' => content_type).merge(auth_headers)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def generate_signature(http_method, uri, body, content_type, date)
|
|
77
|
+
normalized_uri = uri.start_with?('/') ? uri : "/#{uri}"
|
|
78
|
+
|
|
79
|
+
sign_string = [
|
|
80
|
+
http_method.upcase,
|
|
81
|
+
body_digest(body),
|
|
82
|
+
content_type,
|
|
83
|
+
date,
|
|
84
|
+
normalized_uri
|
|
85
|
+
].join("\n")
|
|
86
|
+
|
|
87
|
+
OpenSSL::HMAC.hexdigest(
|
|
88
|
+
signature_digest,
|
|
89
|
+
@config.secret_key,
|
|
90
|
+
sign_string
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def body_digest(body)
|
|
95
|
+
OpenSSL::Digest.new(BODY_DIGEST_NAME).hexdigest(body)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def signature_digest
|
|
99
|
+
OpenSSL::Digest.new(SIGNATURE_DIGEST_NAME)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
# Handles API errors and converts them to appropriate exceptions.
|
|
7
|
+
#
|
|
8
|
+
# This module is included in API base classes to provide consistent error handling
|
|
9
|
+
# across all API requests. It parses error responses and raises typed exceptions.
|
|
10
|
+
#
|
|
11
|
+
# @see Uploadcare::Api::Rest
|
|
12
|
+
# @see Uploadcare::Api::Upload
|
|
13
|
+
module Uploadcare::Internal::ErrorHandler
|
|
14
|
+
# Handle a failed API request and raise an appropriate exception.
|
|
15
|
+
#
|
|
16
|
+
# Parses the error response and raises a typed exception based on the HTTP status code.
|
|
17
|
+
# Also handles Upload API errors which return status 200 with error details in the body.
|
|
18
|
+
#
|
|
19
|
+
# @param error [Faraday::Error] The error from the HTTP client
|
|
20
|
+
# @raise [Uploadcare::Exception::InvalidRequestError] for 400 Bad Request
|
|
21
|
+
# @raise [Uploadcare::Exception::NotFoundError] for 404 Not Found
|
|
22
|
+
# @raise [Uploadcare::Exception::ThrottleError] for 429 Too Many Requests
|
|
23
|
+
# @raise [Uploadcare::Exception::RequestError] for other error statuses
|
|
24
|
+
def handle_error(error)
|
|
25
|
+
response = error.response
|
|
26
|
+
return raise Uploadcare::Exception::RequestError, error.message if response.nil?
|
|
27
|
+
|
|
28
|
+
catch_upload_errors(response)
|
|
29
|
+
|
|
30
|
+
error_message = extract_error_message(response)
|
|
31
|
+
raise_status_error(response, error_message)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Extract error message from response body.
|
|
37
|
+
#
|
|
38
|
+
# @param response [Hash] Response hash with :body key
|
|
39
|
+
# @return [String] Extracted error message
|
|
40
|
+
def extract_error_message(response)
|
|
41
|
+
parsed = JSON.parse(response[:body].to_s)
|
|
42
|
+
return parsed['detail'] if parsed.is_a?(Hash) && parsed['detail']
|
|
43
|
+
return parsed.map { |k, v| "#{k}: #{v}" }.join('; ') if parsed.is_a?(Hash)
|
|
44
|
+
|
|
45
|
+
parsed.to_s
|
|
46
|
+
rescue JSON::ParserError
|
|
47
|
+
response[:body].to_s
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Raise appropriate error based on HTTP status code.
|
|
51
|
+
#
|
|
52
|
+
# @param response [Hash] Response hash with :status key
|
|
53
|
+
# @param message [String] Error message
|
|
54
|
+
# @raise [Uploadcare::Exception::InvalidRequestError] for 400
|
|
55
|
+
# @raise [Uploadcare::Exception::NotFoundError] for 404
|
|
56
|
+
# @raise [Uploadcare::Exception::ThrottleError] for 429
|
|
57
|
+
# @raise [Uploadcare::Exception::RequestError] for other statuses
|
|
58
|
+
def raise_status_error(response, message)
|
|
59
|
+
status = response.is_a?(Hash) ? response[:status] : response
|
|
60
|
+
raise Uploadcare::Exception::InvalidRequestError, message if status == 400
|
|
61
|
+
raise Uploadcare::Exception::NotFoundError, message if status == 404
|
|
62
|
+
return raise_throttle_error(response, message) if status == 429
|
|
63
|
+
|
|
64
|
+
raise Uploadcare::Exception::RequestError, message
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Upload API returns its errors with code 200, and stores its actual code and details
|
|
68
|
+
# within the response message. This method detects that and raises an appropriate error.
|
|
69
|
+
#
|
|
70
|
+
# @param response [Hash] Response hash
|
|
71
|
+
def catch_upload_errors(response)
|
|
72
|
+
return unless response[:status] == 200
|
|
73
|
+
|
|
74
|
+
parsed_response = JSON.parse(response[:body].to_s)
|
|
75
|
+
error = parsed_response['error'] if parsed_response.is_a?(Hash)
|
|
76
|
+
raise Uploadcare::Exception::RequestError, error if error
|
|
77
|
+
rescue JSON::ParserError
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Raise a throttle error with retry-after timeout.
|
|
82
|
+
#
|
|
83
|
+
# @param response [Hash] Response hash
|
|
84
|
+
# @param message [String] Error message
|
|
85
|
+
# @raise [Uploadcare::Exception::ThrottleError]
|
|
86
|
+
def raise_throttle_error(response, message)
|
|
87
|
+
headers = response.is_a?(Hash) ? response[:headers] : nil
|
|
88
|
+
retry_after = headers && (headers['retry-after'] || headers['Retry-After'])
|
|
89
|
+
timeout =
|
|
90
|
+
if retry_after.to_s.match?(/\A\d+(\.\d+)?\z/)
|
|
91
|
+
retry_after.to_f
|
|
92
|
+
else
|
|
93
|
+
begin
|
|
94
|
+
[Time.httpdate(retry_after.to_s) - Time.now, 0].max
|
|
95
|
+
rescue ArgumentError
|
|
96
|
+
0
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
timeout = 10.0 if timeout <= 0
|
|
100
|
+
raise Uploadcare::Exception::ThrottleError.new(message, timeout: timeout)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
# Generates HMAC-SHA256 signatures for signed uploads.
|
|
6
|
+
#
|
|
7
|
+
# Used when `config.sign_uploads` is enabled to generate authentication
|
|
8
|
+
# signatures for Upload API requests.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# Uploadcare::Internal::SignatureGenerator.call(config: config)
|
|
12
|
+
# # => { signature: "abc123...", expire: 1234567890 }
|
|
13
|
+
class Uploadcare::Internal::SignatureGenerator
|
|
14
|
+
# Generate signature params for signed uploads.
|
|
15
|
+
#
|
|
16
|
+
# @param config [Uploadcare::Configuration] Configuration with secret key and lifetime
|
|
17
|
+
# @return [Hash] Hash with :signature and :expire keys
|
|
18
|
+
# @raise [ArgumentError] if secret_key is empty or lifetime is invalid
|
|
19
|
+
def self.call(config: Uploadcare.configuration)
|
|
20
|
+
secret_key = config.secret_key.to_s
|
|
21
|
+
lifetime = config.upload_signature_lifetime
|
|
22
|
+
raise ArgumentError, 'secret_key is required for upload signature' if secret_key.empty?
|
|
23
|
+
unless lifetime.is_a?(Integer) && lifetime.positive?
|
|
24
|
+
raise ArgumentError, 'upload_signature_lifetime must be a positive Integer'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
expires_at = Time.now.to_i + lifetime
|
|
28
|
+
signature = OpenSSL::HMAC.hexdigest('sha256', secret_key, expires_at.to_s)
|
|
29
|
+
{ signature: signature, expire: expires_at }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Handles API rate limiting (throttling) with automatic retry.
|
|
4
|
+
#
|
|
5
|
+
# This module is included in API base classes to provide automatic retry logic
|
|
6
|
+
# when the API returns a throttle error (HTTP 429). It respects the retry-after
|
|
7
|
+
# header (via ThrottleError#timeout) and implements exponential backoff.
|
|
8
|
+
#
|
|
9
|
+
# @see https://uploadcare.com/docs/api_reference/rest/rate_limiting/
|
|
10
|
+
module Uploadcare::Internal::ThrottleHandler
|
|
11
|
+
# Execute a block with automatic retry on throttle errors.
|
|
12
|
+
#
|
|
13
|
+
# Wraps an HTTP request and automatically retries if a ThrottleError is raised.
|
|
14
|
+
# Sleep duration between retries is determined by the error's timeout value
|
|
15
|
+
# with exponential backoff.
|
|
16
|
+
#
|
|
17
|
+
# @param max_attempts [Integer, nil] Maximum retry attempts (defaults to config value)
|
|
18
|
+
# @yield Block containing the HTTP request to execute
|
|
19
|
+
# @return [Object] The result of the block execution
|
|
20
|
+
# @raise [Uploadcare::Exception::ThrottleError] if max retry attempts exceeded
|
|
21
|
+
def handle_throttling(max_attempts: nil)
|
|
22
|
+
attempts = max_attempts
|
|
23
|
+
if attempts.nil?
|
|
24
|
+
attempts = respond_to?(:config) ? config.max_throttle_attempts : Uploadcare.configuration.max_throttle_attempts
|
|
25
|
+
end
|
|
26
|
+
attempts = attempts.to_i
|
|
27
|
+
raise ArgumentError, 'max_attempts must be at least 1' if attempts < 1
|
|
28
|
+
|
|
29
|
+
(attempts - 1).times do |index|
|
|
30
|
+
return yield
|
|
31
|
+
rescue Uploadcare::Exception::ThrottleError => e
|
|
32
|
+
sleep(e.timeout * (2**index))
|
|
33
|
+
end
|
|
34
|
+
yield
|
|
35
|
+
end
|
|
36
|
+
end
|