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,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ # Group resource representing a collection of files in Uploadcare.
6
+ #
7
+ # @see https://uploadcare.com/api-refs/rest-api/v0.7.0/#tag/Group
8
+ class Uploadcare::Resources::Group < Uploadcare::Resources::BaseResource
9
+ # API fields assigned onto group resources.
10
+ ATTRIBUTES = %i[
11
+ id datetime_removed datetime_stored datetime_uploaded is_image is_ready mime_type original_file_url cdn_url
12
+ original_filename size url uuid variations content_info metadata appdata source datetime_created files_count
13
+ files
14
+ ].freeze
15
+
16
+ attr_writer :id, :cdn_url
17
+ attr_accessor :datetime_removed, :datetime_stored, :datetime_uploaded, :is_image, :is_ready, :mime_type,
18
+ :original_file_url, :original_filename, :size, :url, :uuid, :variations,
19
+ :content_info, :metadata, :appdata, :source, :datetime_created, :files_count, :files
20
+
21
+ # --- Class methods ---
22
+
23
+ # List groups with optional filtering and pagination.
24
+ #
25
+ # @param params [Hash] Query parameters
26
+ # @param client [Uploadcare::Client, nil] Client instance
27
+ # @param config [Uploadcare::Configuration] Configuration fallback
28
+ # @param request_options [Hash] Request options
29
+ # @return [Uploadcare::Collections::Paginated]
30
+ def self.list(params: {}, client: nil, config: Uploadcare.configuration, request_options: {})
31
+ resolved_client = resolve_client(client: client, config: config)
32
+ response = Uploadcare::Result.unwrap(
33
+ resolved_client.api.rest.groups.list(params: params, request_options: request_options)
34
+ )
35
+ groups = response['results'].map { |data| new(data, resolved_client) }
36
+
37
+ Uploadcare::Collections::Paginated.new(
38
+ resources: groups,
39
+ next_page: response['next'],
40
+ previous_page: response['previous'],
41
+ per_page: response['per_page'],
42
+ total: response['total'],
43
+ api_client: resolved_client.api.rest.groups,
44
+ resource_class: self,
45
+ client: resolved_client,
46
+ request_options: request_options
47
+ )
48
+ end
49
+
50
+ # Find a group by ID.
51
+ #
52
+ # @param group_id [String] Group UUID (formatted as UUID~size)
53
+ # @param client [Uploadcare::Client, nil] Client instance
54
+ # @param config [Uploadcare::Configuration] Configuration fallback
55
+ # @param request_options [Hash] Request options
56
+ # @return [Uploadcare::Resources::Group]
57
+ def self.find(group_id:, client: nil, config: Uploadcare.configuration, request_options: {})
58
+ resolved_client = resolve_client(client: client, config: config)
59
+ response = Uploadcare::Result.unwrap(
60
+ resolved_client.api.rest.groups.info(uuid: group_id, request_options: request_options)
61
+ )
62
+ new(response, resolved_client)
63
+ end
64
+
65
+ class << self
66
+ alias retrieve find
67
+ alias info find
68
+ end
69
+
70
+ # Create a group from file UUIDs.
71
+ #
72
+ # @param uuids [Array<String>] File UUIDs
73
+ # @param client [Uploadcare::Client, nil] Client instance
74
+ # @param config [Uploadcare::Configuration] Configuration fallback
75
+ # @param options [Hash] Additional options
76
+ # @param request_options [Hash] Request options
77
+ # @return [Uploadcare::Resources::Group]
78
+ def self.create(uuids:, client: nil, config: Uploadcare.configuration, request_options: {}, **options)
79
+ resolved_client = resolve_client(client: client, config: config)
80
+ response = Uploadcare::Result.unwrap(
81
+ resolved_client.api.upload.groups.create(
82
+ files: uuids, request_options: request_options, **options
83
+ )
84
+ )
85
+ new(response, resolved_client)
86
+ end
87
+
88
+ # --- Instance methods ---
89
+
90
+ # Reload group information from the API.
91
+ #
92
+ # @param request_options [Hash] Request options
93
+ # @return [self]
94
+ def reload(request_options: {})
95
+ response = Uploadcare::Result.unwrap(
96
+ client.api.rest.groups.info(uuid: id, request_options: request_options)
97
+ )
98
+ assign_attributes(response)
99
+ self
100
+ end
101
+ alias load reload
102
+
103
+ # Delete this group.
104
+ #
105
+ # @param request_options [Hash] Request options
106
+ # @return [nil]
107
+ def delete(request_options: {})
108
+ Uploadcare::Result.unwrap(
109
+ client.api.rest.groups.delete(uuid: id, request_options: request_options)
110
+ )
111
+ end
112
+
113
+ # Returns group ID, extracting from CDN URL if needed.
114
+ #
115
+ # @return [String, nil]
116
+ def id
117
+ return @id if @id
118
+ return @uuid if defined?(@uuid) && !@uuid.to_s.empty?
119
+ return unless @cdn_url
120
+
121
+ uri = URI.parse(@cdn_url)
122
+ @id = uri.path.split('/').reject(&:empty?).first
123
+ end
124
+
125
+ # Returns the CDN URL for this group.
126
+ #
127
+ # @return [String]
128
+ def cdn_url
129
+ return @cdn_url if @cdn_url && !@cdn_url.empty?
130
+
131
+ "#{config.cdn_base}#{id}/"
132
+ end
133
+
134
+ # Returns CDN URLs for all files in the group.
135
+ #
136
+ # @return [Array<String>]
137
+ def file_cdn_urls
138
+ return [] if files_count.nil?
139
+
140
+ files_count.times.map { |i| "#{cdn_url}nth/#{i}/" }
141
+ end
142
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Project resource representing the current Uploadcare project.
4
+ #
5
+ # @see https://uploadcare.com/api-refs/rest-api/v0.7.0/#tag/Project
6
+ class Uploadcare::Resources::Project < Uploadcare::Resources::BaseResource
7
+ attr_accessor :name, :pub_key, :autostore_enabled, :collaborators
8
+
9
+ # Get current project information.
10
+ #
11
+ # @param client [Uploadcare::Client, nil] Client instance
12
+ # @param config [Uploadcare::Configuration] Configuration fallback
13
+ # @param request_options [Hash] Request options
14
+ # @return [Uploadcare::Resources::Project]
15
+ def self.current(client: nil, config: Uploadcare.configuration, request_options: {})
16
+ resolved_client = resolve_client(client: client, config: config)
17
+ response = Uploadcare::Result.unwrap(
18
+ resolved_client.api.rest.project.show(request_options: request_options)
19
+ )
20
+ new(response, resolved_client)
21
+ end
22
+
23
+ class << self
24
+ alias show current
25
+ end
26
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Video conversion resource.
4
+ #
5
+ # @see https://uploadcare.com/api-refs/rest-api/v0.7.0/#tag/Video
6
+ class Uploadcare::Resources::VideoConversion < Uploadcare::Resources::BaseResource
7
+ attr_accessor :problems, :status, :error, :result
8
+
9
+ # Convert a video to a specified format (class method).
10
+ #
11
+ # @param params [Hash] Conversion parameters (:uuid, :format, :quality)
12
+ # @param options [Hash] Optional parameters (:store)
13
+ # @param client [Uploadcare::Client, nil] Client instance
14
+ # @param config [Uploadcare::Configuration] Configuration fallback
15
+ # @param request_options [Hash] Request options
16
+ # @return [Uploadcare::Resources::VideoConversion]
17
+ def self.convert(params:, options: {}, client: nil, config: Uploadcare.configuration, request_options: {})
18
+ raise ArgumentError, 'params must include :uuid' unless params[:uuid]
19
+ raise ArgumentError, 'params must include :format' unless params[:format]
20
+ raise ArgumentError, 'params must include :quality' unless params[:quality]
21
+
22
+ paths = Array(params[:uuid]).map do |uuid|
23
+ "#{uuid}/video/-/format/#{params[:format]}/-/quality/#{params[:quality]}/"
24
+ end
25
+
26
+ resolved_client = resolve_client(client: client, config: config)
27
+ response = Uploadcare::Result.unwrap(
28
+ resolved_client.api.rest.video_conversions.convert(
29
+ paths: paths, options: options, request_options: request_options
30
+ )
31
+ )
32
+ new(response, resolved_client)
33
+ end
34
+
35
+ # Fetch video conversion status for a job token.
36
+ #
37
+ # @param token [String]
38
+ # @param client [Uploadcare::Client, nil]
39
+ # @param config [Uploadcare::Configuration]
40
+ # @param request_options [Hash]
41
+ # @return [Uploadcare::Resources::VideoConversion]
42
+ def self.status(token:, client: nil, config: Uploadcare.configuration, request_options: {})
43
+ resolved_client = resolve_client(client: client, config: config)
44
+ new({}, resolved_client).fetch_status(token: token, request_options: request_options)
45
+ end
46
+
47
+ # Refresh this resource from the conversion status endpoint.
48
+ #
49
+ # @param token [String]
50
+ # @param request_options [Hash]
51
+ # @return [self]
52
+ def fetch_status(token:, request_options: {})
53
+ response = Uploadcare::Result.unwrap(
54
+ client.api.rest.video_conversions.status(token: token, request_options: request_options)
55
+ )
56
+ assign_attributes(response)
57
+ self
58
+ end
59
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Webhook resource for managing Uploadcare webhooks.
4
+ #
5
+ # @see https://uploadcare.com/api-refs/rest-api/v0.7.0/#tag/Webhook
6
+ class Uploadcare::Resources::Webhook < Uploadcare::Resources::BaseResource
7
+ attr_accessor :id, :project, :created, :updated, :event, :target_url, :is_active, :signing_secret, :version
8
+
9
+ # List all project webhooks.
10
+ #
11
+ # @param client [Uploadcare::Client, nil] Client instance
12
+ # @param config [Uploadcare::Configuration] Configuration fallback
13
+ # @param request_options [Hash] Request options
14
+ # @return [Array<Uploadcare::Resources::Webhook>]
15
+ def self.list(client: nil, config: Uploadcare.configuration, request_options: {})
16
+ resolved_client = resolve_client(client: client, config: config)
17
+ response = Uploadcare::Result.unwrap(
18
+ resolved_client.api.rest.webhooks.list(request_options: request_options)
19
+ )
20
+ response.map { |data| new(data, resolved_client) }
21
+ end
22
+
23
+ # Create a new webhook.
24
+ #
25
+ # @param target_url [String] Webhook target URL
26
+ # @param options [Hash] Webhook options
27
+ # @option options [String] :event Event type (default: "file.uploaded")
28
+ # @option options [Boolean] :is_active Active flag (default: true)
29
+ # @option options [String] :signing_secret Signing secret
30
+ # @option options [String] :version Webhook payload version
31
+ # @param client [Uploadcare::Client, nil] Client instance
32
+ # @param config [Uploadcare::Configuration] Configuration fallback
33
+ # @param request_options [Hash] Request options
34
+ # @return [Uploadcare::Resources::Webhook]
35
+ def self.create(target_url:, client: nil, config: Uploadcare.configuration, request_options: {}, **options)
36
+ resolved_client = resolve_client(client: client, config: config)
37
+ event = options.fetch(:event, 'file.uploaded')
38
+ is_active = options.key?(:is_active) ? options[:is_active] : true
39
+ payload = { target_url: target_url, event: event, is_active: is_active }
40
+ payload[:signing_secret] = options[:signing_secret] if options[:signing_secret]
41
+ payload[:version] = options[:version] if options[:version]
42
+
43
+ response = Uploadcare::Result.unwrap(
44
+ resolved_client.api.rest.webhooks.create(options: payload, request_options: request_options)
45
+ )
46
+ new(response, resolved_client)
47
+ end
48
+
49
+ # Update a webhook.
50
+ #
51
+ # @param id [Integer] Webhook ID
52
+ # @param options [Hash] Webhook options to update
53
+ # @param client [Uploadcare::Client, nil] Client instance
54
+ # @param config [Uploadcare::Configuration] Configuration fallback
55
+ # @param request_options [Hash] Request options
56
+ # @return [Uploadcare::Resources::Webhook]
57
+ def self.update(id:, client: nil, config: Uploadcare.configuration, request_options: {}, **options)
58
+ resolved_client = resolve_client(client: client, config: config)
59
+ payload = options.slice(:target_url, :event, :signing_secret, :version)
60
+ payload[:is_active] = options[:is_active] if options.key?(:is_active)
61
+
62
+ response = Uploadcare::Result.unwrap(
63
+ resolved_client.api.rest.webhooks.update(id: id, options: payload, request_options: request_options)
64
+ )
65
+ new(response, resolved_client)
66
+ end
67
+
68
+ # Delete a webhook by target URL.
69
+ #
70
+ # @param target_url [String] Target URL of the webhook to delete
71
+ # @param client [Uploadcare::Client, nil] Client instance
72
+ # @param config [Uploadcare::Configuration] Configuration fallback
73
+ # @param request_options [Hash] Request options
74
+ # @return [nil]
75
+ def self.delete(target_url:, client: nil, config: Uploadcare.configuration, request_options: {})
76
+ resolved_client = resolve_client(client: client, config: config)
77
+ Uploadcare::Result.unwrap(
78
+ resolved_client.api.rest.webhooks.delete(target_url: target_url, request_options: request_options)
79
+ )
80
+ end
81
+
82
+ class << self
83
+ alias unsubscribe delete
84
+ end
85
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Result wrapper for success/error handling.
4
+ class Uploadcare::Result
5
+ attr_reader :value, :error
6
+
7
+ def initialize(value: nil, error: nil)
8
+ @value = value
9
+ @error = error
10
+ end
11
+
12
+ # Build a success result.
13
+ #
14
+ # @param value [Object]
15
+ # @return [Uploadcare::Result]
16
+ def self.success(value)
17
+ new(value: value)
18
+ end
19
+
20
+ # Build a failure result.
21
+ #
22
+ # @param error [Object]
23
+ # @return [Uploadcare::Result]
24
+ def self.failure(error)
25
+ new(error: error)
26
+ end
27
+
28
+ # Capture exceptions and wrap in Result.
29
+ #
30
+ # @return [Uploadcare::Result]
31
+ def self.capture
32
+ success(yield)
33
+ rescue StandardError => e
34
+ failure(e)
35
+ end
36
+
37
+ # Unwrap a Result or return the value as-is.
38
+ #
39
+ # @param value [Object]
40
+ # @return [Object]
41
+ def self.unwrap(value)
42
+ value.is_a?(Uploadcare::Result) ? value.value! : value
43
+ end
44
+
45
+ def success?
46
+ @error.nil?
47
+ end
48
+
49
+ def failure?
50
+ !success?
51
+ end
52
+
53
+ # @return [Object] success value
54
+ def success
55
+ @value
56
+ end
57
+
58
+ # @return [Object] error value
59
+ def failure
60
+ @error
61
+ end
62
+
63
+ # Return the success value or raise the wrapped error.
64
+ #
65
+ # @return [Object]
66
+ # @raise [Exception, RuntimeError]
67
+ def value!
68
+ if failure?
69
+ error = @error
70
+ raise error if error.is_a?(Exception)
71
+ raise error if error.is_a?(String)
72
+
73
+ raise error.to_s
74
+ end
75
+
76
+ @value
77
+ end
78
+
79
+ # @return [String, nil] error message
80
+ def error_message
81
+ return nil if @error.nil?
82
+
83
+ @error.respond_to?(:message) ? @error.message : @error.to_s
84
+ end
85
+ end
@@ -1,63 +1,67 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'openssl'
3
4
  require_relative 'base_generator'
4
5
 
5
- module Uploadcare
6
- module SignedUrlGenerators
7
- class AkamaiGenerator < Uploadcare::SignedUrlGenerators::BaseGenerator
8
- UUID_REGEX = '[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}'
9
- TEMPLATE = 'https://{cdn_host}/{uuid}/?token=exp={expiration}{delimiter}acl={acl}{delimiter}hmac={token}'
10
-
11
- def generate_url(uuid, acl = uuid, wildcard: false)
12
- raise ArgumentError, 'Must contain valid UUID' unless valid?(uuid)
13
-
14
- formatted_acl = build_acl(uuid, acl, wildcard: wildcard)
15
- expire = build_expire
16
- signature = build_signature(expire, formatted_acl)
17
-
18
- TEMPLATE.gsub('{delimiter}', delimiter)
19
- .sub('{cdn_host}', sanitized_string(cdn_host))
20
- .sub('{uuid}', sanitized_string(uuid))
21
- .sub('{acl}', formatted_acl)
22
- .sub('{expiration}', expire)
23
- .sub('{token}', signature)
24
- end
25
-
26
- private
27
-
28
- def valid?(uuid)
29
- uuid.match(UUID_REGEX)
30
- end
31
-
32
- def delimiter
33
- '~'
34
- end
35
-
36
- def build_acl(uuid, acl, wildcard: false)
37
- if wildcard
38
- "/#{sanitized_string(uuid)}/*"
39
- else
40
- "/#{sanitized_string(acl)}/"
41
- end
42
- end
43
-
44
- def build_expire
45
- (Time.now.to_i + ttl).to_s
46
- end
47
-
48
- def build_signature(expire, acl)
49
- signature = ["exp=#{expire}", "acl=#{acl}"].join(delimiter)
50
- secret_key_bin = Array(secret_key.gsub(/\s/, '')).pack('H*')
51
- OpenSSL::HMAC.hexdigest(algorithm, secret_key_bin, signature)
52
- end
53
-
54
- # rubocop:disable Style/SlicingWithRange
55
- def sanitized_string(string)
56
- string = string[1..-1] if string[0] == '/'
57
- string = string[0...-1] if string[-1] == '/'
58
- string.strip
59
- end
60
- # rubocop:enable Style/SlicingWithRange
6
+ # Akamai signed URL generator.
7
+ class Uploadcare::SignedUrlGenerators::AkamaiGenerator < Uploadcare::SignedUrlGenerators::BaseGenerator
8
+ # UUID validation regex.
9
+ UUID_REGEX = /\A[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}\z/i
10
+ # Akamai token template.
11
+ TEMPLATE = 'https://{cdn_host}/{uuid}/?token=exp={expiration}{delimiter}acl={acl}{delimiter}hmac={token}'
12
+
13
+ def generate_url(uuid, acl = uuid, wildcard: false)
14
+ raise ArgumentError, 'Must contain valid UUID' unless valid?(uuid)
15
+
16
+ formatted_acl = build_acl(uuid, acl, wildcard: wildcard)
17
+ expire = build_expire
18
+ signature = build_signature(expire, formatted_acl)
19
+
20
+ TEMPLATE.gsub('{delimiter}', delimiter)
21
+ .sub('{cdn_host}', sanitized_string(cdn_host))
22
+ .sub('{uuid}', sanitized_string(uuid))
23
+ .sub('{acl}', formatted_acl)
24
+ .sub('{expiration}', expire)
25
+ .sub('{token}', signature)
26
+ end
27
+
28
+ private
29
+
30
+ def valid?(uuid)
31
+ raise ArgumentError, 'Must contain valid UUID' unless uuid.is_a?(String)
32
+
33
+ UUID_REGEX.match?(uuid)
34
+ end
35
+
36
+ def delimiter
37
+ '~'
38
+ end
39
+
40
+ def build_acl(uuid, acl, wildcard: false)
41
+ if wildcard
42
+ "/#{sanitized_delimiter_path(uuid)}/*"
43
+ else
44
+ "/#{sanitized_delimiter_path(acl)}/"
61
45
  end
62
46
  end
47
+
48
+ def sanitized_delimiter_path(path)
49
+ sanitized_string(path).gsub('~') { |escape_char| "%#{escape_char.ord.to_s(16).downcase}" }
50
+ end
51
+
52
+ def build_expire
53
+ (Time.now.to_i + ttl).to_s
54
+ end
55
+
56
+ def build_signature(expire, acl)
57
+ signature = ["exp=#{expire}", "acl=#{acl}"].join(delimiter)
58
+ secret_key_bin = Array(secret_key.delete(" \t\r\n")).pack('H*')
59
+ OpenSSL::HMAC.hexdigest(algorithm, secret_key_bin, signature)
60
+ end
61
+
62
+ def sanitized_string(string)
63
+ string = string[1..] if string[0] == '/'
64
+ string = string[0...-1] if string[-1] == '/'
65
+ string.strip
66
+ end
63
67
  end
@@ -1,21 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Uploadcare
4
- module SignedUrlGenerators
5
- class BaseGenerator
6
- attr_accessor :cdn_host, :ttl, :algorithm
7
- attr_reader :secret_key
3
+ # Base class for signed URL generators.
4
+ class Uploadcare::SignedUrlGenerators::BaseGenerator
5
+ attr_accessor :cdn_host, :ttl, :algorithm
6
+ attr_reader :secret_key
8
7
 
9
- def initialize(cdn_host:, secret_key:, ttl: 300, algorithm: 'sha256')
10
- @ttl = ttl
11
- @algorithm = algorithm
12
- @cdn_host = cdn_host
13
- @secret_key = secret_key
14
- end
8
+ def initialize(cdn_host:, secret_key:, ttl: 300, algorithm: 'sha256')
9
+ @ttl = ttl
10
+ @algorithm = algorithm
11
+ @cdn_host = cdn_host
12
+ @secret_key = secret_key
13
+ end
15
14
 
16
- def generate_url
17
- raise NotImplementedError, "#{__method__} method not present"
18
- end
19
- end
15
+ # Generate a signed URL.
16
+ #
17
+ # @return [String]
18
+ def generate_url
19
+ raise NotImplementedError, "#{__method__} method not present"
20
20
  end
21
21
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Root namespace for the Uploadcare Ruby SDK.
4
+ module Uploadcare
5
+ # Gem version.
6
+ VERSION = '5.0.0.rc1'
7
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ # This object verifies a signature received along with webhook headers
6
+ class Uploadcare::WebhookSignatureVerifier
7
+ # @see https://uploadcare.com/docs/security/secure-webhooks/
8
+ def self.valid?(webhook_body: nil, signing_secret: nil, x_uc_signature_header: nil)
9
+ webhook_body_json = webhook_body
10
+ signing_secret ||= ENV.fetch('UC_SIGNING_SECRET', nil)
11
+
12
+ return false unless valid_parameters?(signing_secret, x_uc_signature_header, webhook_body_json)
13
+
14
+ calculated_signature = calculate_signature(signing_secret, webhook_body_json)
15
+
16
+ # Use constant-time comparison to prevent timing attacks
17
+ secure_compare?(calculated_signature, x_uc_signature_header)
18
+ end
19
+
20
+ # Check if all required parameters are present and non-empty
21
+ # @param signing_secret [String] signing secret
22
+ # @param signature_header [String] signature from header
23
+ # @param body [String] webhook body
24
+ # @return [Boolean] true if all parameters are valid
25
+ def self.valid_parameters?(signing_secret, signature_header, body)
26
+ return false if signing_secret.nil? || signing_secret.to_s.empty?
27
+ return false if signature_header.nil? || signature_header.to_s.empty?
28
+ return false if body.nil? || body.to_s.empty?
29
+
30
+ true
31
+ end
32
+
33
+ # Calculate HMAC signature for webhook body
34
+ # @param secret [String] signing secret
35
+ # @param body [String] webhook body JSON
36
+ # @return [String] calculated signature
37
+ def self.calculate_signature(secret, body)
38
+ digest = OpenSSL::Digest.new('sha256')
39
+ "v1=#{OpenSSL::HMAC.hexdigest(digest, secret, body)}"
40
+ end
41
+
42
+ # Constant-time string comparison to prevent timing attacks
43
+ # @param first [String] first string
44
+ # @param second [String] second string
45
+ # @return [Boolean] true if strings are equal
46
+ def self.secure_compare?(first, second)
47
+ return false if first.nil? || second.nil?
48
+ return false unless first.bytesize == second.bytesize
49
+
50
+ OpenSSL.fixed_length_secure_compare(first, second)
51
+ rescue NoMethodError
52
+ result = 0
53
+ index = 0
54
+ while index < first.bytesize
55
+ result |= first.getbyte(index) ^ second.getbyte(index)
56
+ index += 1
57
+ end
58
+ result.zero?
59
+ end
60
+ end