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.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +7 -0
  3. data/.github/workflows/gem-push.yml +1 -1
  4. data/.github/workflows/ruby.yml +10 -13
  5. data/.gitignore +9 -0
  6. data/.rubocop.yml +95 -8
  7. data/CHANGELOG.md +78 -0
  8. data/Gemfile +23 -6
  9. data/MIGRATING_V5.md +290 -0
  10. data/README.md +422 -671
  11. data/Rakefile +5 -1
  12. data/api_examples/README.md +77 -0
  13. data/api_examples/rest_api/delete_files_storage.rb +3 -5
  14. data/api_examples/rest_api/delete_files_uuid_metadata_key.rb +3 -4
  15. data/api_examples/rest_api/delete_files_uuid_storage.rb +3 -4
  16. data/api_examples/rest_api/delete_groups_uuid.rb +3 -4
  17. data/api_examples/rest_api/delete_webhooks_unsubscribe.rb +3 -4
  18. data/api_examples/rest_api/get_addons_aws_rekognition_detect_labels_execute_status.rb +3 -6
  19. data/api_examples/rest_api/get_addons_aws_rekognition_detect_moderation_labels_execute_status.rb +3 -6
  20. data/api_examples/rest_api/get_addons_remove_bg_execute_status.rb +3 -6
  21. data/api_examples/rest_api/get_addons_uc_clamav_virus_scan_execute_status.rb +3 -6
  22. data/api_examples/rest_api/get_convert_document_status_token.rb +3 -5
  23. data/api_examples/rest_api/get_convert_document_uuid.rb +3 -5
  24. data/api_examples/rest_api/get_convert_video_status_token.rb +3 -5
  25. data/api_examples/rest_api/get_files.rb +3 -5
  26. data/api_examples/rest_api/get_files_uuid.rb +3 -5
  27. data/api_examples/rest_api/get_files_uuid_metadata.rb +3 -5
  28. data/api_examples/rest_api/get_files_uuid_metadata_key.rb +3 -5
  29. data/api_examples/rest_api/get_groups.rb +3 -5
  30. data/api_examples/rest_api/get_groups_uuid.rb +3 -5
  31. data/api_examples/rest_api/get_project.rb +3 -5
  32. data/api_examples/rest_api/get_webhooks.rb +3 -5
  33. data/api_examples/rest_api/post_addons_aws_rekognition_detect_labels_execute.rb +3 -5
  34. data/api_examples/rest_api/post_addons_aws_rekognition_detect_moderation_labels_execute.rb +3 -5
  35. data/api_examples/rest_api/post_addons_remove_bg_execute.rb +3 -5
  36. data/api_examples/rest_api/post_addons_uc_clamav_virus_scan_execute.rb +3 -5
  37. data/api_examples/rest_api/post_convert_document.rb +3 -6
  38. data/api_examples/rest_api/post_convert_video.rb +3 -10
  39. data/api_examples/rest_api/post_files_local_copy.rb +3 -6
  40. data/api_examples/rest_api/post_files_remote_copy.rb +3 -7
  41. data/api_examples/rest_api/post_webhooks.rb +3 -9
  42. data/api_examples/rest_api/put_files_storage.rb +3 -8
  43. data/api_examples/rest_api/put_files_uuid_metadata_key.rb +3 -7
  44. data/api_examples/rest_api/put_files_uuid_storage.rb +3 -5
  45. data/api_examples/rest_api/put_webhooks_id.rb +3 -11
  46. data/api_examples/support/example_helper.rb +250 -0
  47. data/api_examples/support/run_rest_example.rb +161 -0
  48. data/api_examples/support/run_upload_example.rb +88 -0
  49. data/api_examples/upload_api/get_from_url_status.rb +3 -5
  50. data/api_examples/upload_api/get_group_info.rb +3 -6
  51. data/api_examples/upload_api/get_info.rb +3 -6
  52. data/api_examples/upload_api/post_base.rb +3 -5
  53. data/api_examples/upload_api/post_from_url.rb +3 -5
  54. data/api_examples/upload_api/post_group.rb +3 -8
  55. data/api_examples/upload_api/post_multipart_complete.rb +3 -7
  56. data/api_examples/upload_api/post_multipart_start.rb +3 -7
  57. data/api_examples/upload_api/put_multipart_part.rb +4 -0
  58. data/bin/console +1 -1
  59. data/docs/release-notes-5.0.0.rc1.md +34 -0
  60. data/examples/README.md +39 -0
  61. data/examples/batch_upload.rb +54 -0
  62. data/examples/group_creation.rb +88 -0
  63. data/examples/large_file_upload.rb +88 -0
  64. data/examples/simple_upload.rb +39 -0
  65. data/examples/upload_with_progress.rb +84 -0
  66. data/examples/url_upload.rb +56 -0
  67. data/lib/uploadcare/api/rest/addons.rb +107 -0
  68. data/lib/uploadcare/api/rest/document_conversions.rb +65 -0
  69. data/lib/uploadcare/api/rest/file_metadata.rb +71 -0
  70. data/lib/uploadcare/api/rest/files.rb +112 -0
  71. data/lib/uploadcare/api/rest/groups.rb +49 -0
  72. data/lib/uploadcare/api/rest/project.rb +23 -0
  73. data/lib/uploadcare/api/rest/video_conversions.rb +52 -0
  74. data/lib/uploadcare/api/rest/webhooks.rb +74 -0
  75. data/lib/uploadcare/api/rest.rb +254 -0
  76. data/lib/uploadcare/api/upload/files.rb +313 -0
  77. data/lib/uploadcare/api/upload/groups.rb +72 -0
  78. data/lib/uploadcare/api/upload.rb +272 -0
  79. data/lib/uploadcare/client/addons_accessor.rb +85 -0
  80. data/lib/uploadcare/client/api.rb +33 -0
  81. data/lib/uploadcare/client/conversions_accessor.rb +33 -0
  82. data/lib/uploadcare/client/document_conversions_accessor.rb +41 -0
  83. data/lib/uploadcare/client/file_metadata_accessor.rb +46 -0
  84. data/lib/uploadcare/client/files_accessor.rb +82 -0
  85. data/lib/uploadcare/client/groups_accessor.rb +35 -0
  86. data/lib/uploadcare/client/project_accessor.rb +17 -0
  87. data/lib/uploadcare/client/video_conversions_accessor.rb +33 -0
  88. data/lib/uploadcare/client/webhooks_accessor.rb +42 -0
  89. data/lib/uploadcare/client.rb +127 -0
  90. data/lib/uploadcare/cname_generator.rb +68 -0
  91. data/lib/uploadcare/collections/batch_result.rb +35 -0
  92. data/lib/uploadcare/collections/paginated.rb +165 -0
  93. data/lib/uploadcare/configuration.rb +81 -0
  94. data/lib/uploadcare/exception/auth_error.rb +2 -6
  95. data/lib/uploadcare/exception/configuration_error.rb +4 -0
  96. data/lib/uploadcare/exception/conversion_error.rb +2 -6
  97. data/lib/uploadcare/exception/invalid_request_error.rb +4 -0
  98. data/lib/uploadcare/exception/multipart_upload_error.rb +4 -0
  99. data/lib/uploadcare/exception/not_found_error.rb +4 -0
  100. data/lib/uploadcare/exception/request_error.rb +2 -6
  101. data/lib/uploadcare/exception/retry_error.rb +2 -6
  102. data/lib/uploadcare/exception/throttle_error.rb +7 -11
  103. data/lib/uploadcare/exception/unknown_status_error.rb +4 -0
  104. data/lib/uploadcare/exception/upload_error.rb +4 -0
  105. data/lib/uploadcare/exception/upload_timeout_error.rb +4 -0
  106. data/lib/uploadcare/internal/authenticator.rb +101 -0
  107. data/lib/uploadcare/internal/error_handler.rb +102 -0
  108. data/lib/uploadcare/internal/signature_generator.rb +31 -0
  109. data/lib/uploadcare/internal/throttle_handler.rb +36 -0
  110. data/lib/uploadcare/internal/upload_io.rb +110 -0
  111. data/lib/uploadcare/internal/upload_params_generator.rb +86 -0
  112. data/lib/uploadcare/internal/user_agent.rb +22 -0
  113. data/lib/uploadcare/operations/multipart_upload.rb +213 -0
  114. data/lib/uploadcare/operations/upload_router.rb +162 -0
  115. data/lib/uploadcare/resources/addon_execution.rb +97 -0
  116. data/lib/uploadcare/resources/base_resource.rb +61 -0
  117. data/lib/uploadcare/resources/document_conversion.rb +81 -0
  118. data/lib/uploadcare/resources/file.rb +366 -0
  119. data/lib/uploadcare/resources/file_metadata.rb +135 -0
  120. data/lib/uploadcare/resources/group.rb +142 -0
  121. data/lib/uploadcare/resources/project.rb +26 -0
  122. data/lib/uploadcare/resources/video_conversion.rb +59 -0
  123. data/lib/uploadcare/resources/webhook.rb +85 -0
  124. data/lib/uploadcare/result.rb +85 -0
  125. data/lib/uploadcare/signed_url_generators/akamai_generator.rb +60 -56
  126. data/lib/uploadcare/signed_url_generators/base_generator.rb +15 -15
  127. data/lib/uploadcare/version.rb +7 -0
  128. data/lib/uploadcare/webhook_signature_verifier.rb +60 -0
  129. data/lib/uploadcare.rb +84 -50
  130. data/mise.toml +2 -0
  131. data/uploadcare-ruby.gemspec +8 -7
  132. metadata +102 -74
  133. data/api_examples/upload_api/put_presigned_url_x.rb +0 -8
  134. data/lib/uploadcare/api/api.rb +0 -25
  135. data/lib/uploadcare/client/addons_client.rb +0 -69
  136. data/lib/uploadcare/client/conversion/base_conversion_client.rb +0 -59
  137. data/lib/uploadcare/client/conversion/document_conversion_client.rb +0 -45
  138. data/lib/uploadcare/client/conversion/video_conversion_client.rb +0 -46
  139. data/lib/uploadcare/client/file_client.rb +0 -48
  140. data/lib/uploadcare/client/file_list_client.rb +0 -46
  141. data/lib/uploadcare/client/file_metadata_client.rb +0 -36
  142. data/lib/uploadcare/client/group_client.rb +0 -45
  143. data/lib/uploadcare/client/multipart_upload/chunks_client.rb +0 -58
  144. data/lib/uploadcare/client/multipart_upload_client.rb +0 -64
  145. data/lib/uploadcare/client/project_client.rb +0 -20
  146. data/lib/uploadcare/client/rest_client.rb +0 -77
  147. data/lib/uploadcare/client/rest_group_client.rb +0 -43
  148. data/lib/uploadcare/client/upload_client.rb +0 -46
  149. data/lib/uploadcare/client/uploader_client.rb +0 -128
  150. data/lib/uploadcare/client/webhook_client.rb +0 -49
  151. data/lib/uploadcare/concern/error_handler.rb +0 -54
  152. data/lib/uploadcare/concern/throttle_handler.rb +0 -25
  153. data/lib/uploadcare/concern/upload_error_handler.rb +0 -32
  154. data/lib/uploadcare/entity/addons.rb +0 -14
  155. data/lib/uploadcare/entity/conversion/base_converter.rb +0 -43
  156. data/lib/uploadcare/entity/conversion/document_converter.rb +0 -15
  157. data/lib/uploadcare/entity/conversion/video_converter.rb +0 -15
  158. data/lib/uploadcare/entity/decorator/paginator.rb +0 -79
  159. data/lib/uploadcare/entity/entity.rb +0 -18
  160. data/lib/uploadcare/entity/file.rb +0 -103
  161. data/lib/uploadcare/entity/file_list.rb +0 -32
  162. data/lib/uploadcare/entity/file_metadata.rb +0 -30
  163. data/lib/uploadcare/entity/group.rb +0 -49
  164. data/lib/uploadcare/entity/group_list.rb +0 -24
  165. data/lib/uploadcare/entity/project.rb +0 -13
  166. data/lib/uploadcare/entity/uploader.rb +0 -93
  167. data/lib/uploadcare/entity/webhook.rb +0 -14
  168. data/lib/uploadcare/param/authentication_header.rb +0 -37
  169. data/lib/uploadcare/param/conversion/document/processing_job_url_builder.rb +0 -39
  170. data/lib/uploadcare/param/conversion/video/processing_job_url_builder.rb +0 -64
  171. data/lib/uploadcare/param/param.rb +0 -10
  172. data/lib/uploadcare/param/secure_auth_header.rb +0 -51
  173. data/lib/uploadcare/param/simple_auth_header.rb +0 -14
  174. data/lib/uploadcare/param/upload/signature_generator.rb +0 -24
  175. data/lib/uploadcare/param/upload/upload_params_generator.rb +0 -41
  176. data/lib/uploadcare/param/user_agent.rb +0 -21
  177. data/lib/uploadcare/param/webhook_signature_verifier.rb +0 -23
  178. 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
- module Uploadcare
4
- module Exception
5
- # Invalid Auth configuration error
6
- class AuthError < StandardError; end
7
- end
8
- end
3
+ # Invalid Auth configuration error
4
+ class Uploadcare::Exception::AuthError < StandardError; end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Standard error for invalid API configuration responses
4
+ class Uploadcare::Exception::ConfigurationError < StandardError; end
@@ -1,8 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Uploadcare
4
- module Exception
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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Specific error for invalid requests (400 Bad Request)
4
+ class Uploadcare::Exception::InvalidRequestError < Uploadcare::Exception::RequestError; end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when multipart upload fails
4
+ class Uploadcare::Exception::MultipartUploadError < Uploadcare::Exception::UploadError; end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Specific error for not found resources (404 Not Found)
4
+ class Uploadcare::Exception::NotFoundError < Uploadcare::Exception::RequestError; end
@@ -1,8 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Uploadcare
4
- module Exception
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
- module Uploadcare
4
- module Exception
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
- module Uploadcare
4
- module Exception
5
- # Exception for throttled requests
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
- # @param timeout [Float] Amount of seconds the request have been throttled for
10
- def initialize(timeout = 10.0)
11
- super
12
- @timeout = timeout
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,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when upload status is unknown
4
+ class Uploadcare::Exception::UnknownStatusError < Uploadcare::Exception::UploadError; end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # General upload error
4
+ class Uploadcare::Exception::UploadError < StandardError; end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when upload times out
4
+ class Uploadcare::Exception::UploadTimeoutError < Uploadcare::Exception::UploadError; 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