uploadcare-ruby 4.4.3 → 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 +71 -1
- 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 -8
- 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 +50 -51
- 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 -60
- 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,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
|
|
5
|
+
# Wraps upload inputs so multipart and direct uploads can treat files and streams uniformly.
|
|
6
|
+
class Uploadcare::Internal::UploadIo
|
|
7
|
+
# Fallback filename used when the source object does not expose one.
|
|
8
|
+
DEFAULT_FILENAME = 'upload.bin'
|
|
9
|
+
|
|
10
|
+
attr_reader :io, :original_filename
|
|
11
|
+
|
|
12
|
+
# Wrap a readable source into a unified upload object.
|
|
13
|
+
#
|
|
14
|
+
# @param source [IO]
|
|
15
|
+
# @param filename [String, nil]
|
|
16
|
+
# @return [Uploadcare::Internal::UploadIo]
|
|
17
|
+
def self.wrap(source, filename: nil)
|
|
18
|
+
raise ArgumentError, 'file must be a readable IO object' unless source.respond_to?(:read)
|
|
19
|
+
|
|
20
|
+
if path_backed?(source)
|
|
21
|
+
new(source, original_filename: filename || extract_filename(source), cleanup: false)
|
|
22
|
+
else
|
|
23
|
+
wrap_stream(source, filename: filename || extract_filename(source))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param source [IO]
|
|
28
|
+
# @return [Boolean]
|
|
29
|
+
def self.path_backed?(source)
|
|
30
|
+
source.respond_to?(:path) &&
|
|
31
|
+
source.path &&
|
|
32
|
+
::File.file?(source.path) &&
|
|
33
|
+
::File.readable?(source.path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param source [IO]
|
|
37
|
+
# @return [String]
|
|
38
|
+
def self.extract_filename(source)
|
|
39
|
+
if source.respond_to?(:original_filename) && source.original_filename && !source.original_filename.empty?
|
|
40
|
+
source.original_filename
|
|
41
|
+
elsif path_backed?(source)
|
|
42
|
+
::File.basename(source.path)
|
|
43
|
+
else
|
|
44
|
+
DEFAULT_FILENAME
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Materialize a non-path-backed stream into a tempfile.
|
|
49
|
+
#
|
|
50
|
+
# @param source [IO]
|
|
51
|
+
# @param filename [String]
|
|
52
|
+
# @return [Uploadcare::Internal::UploadIo]
|
|
53
|
+
def self.wrap_stream(source, filename:)
|
|
54
|
+
extension = ::File.extname(filename.to_s)
|
|
55
|
+
tempfile = Tempfile.new(['uploadcare-upload', extension.empty? ? '.bin' : extension])
|
|
56
|
+
tempfile.binmode
|
|
57
|
+
|
|
58
|
+
source.rewind if source.respond_to?(:rewind)
|
|
59
|
+
IO.copy_stream(source, tempfile)
|
|
60
|
+
tempfile.rewind
|
|
61
|
+
source.rewind if source.respond_to?(:rewind)
|
|
62
|
+
|
|
63
|
+
new(tempfile, original_filename: filename, cleanup: true)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param io [IO]
|
|
67
|
+
# @param original_filename [String]
|
|
68
|
+
# @param cleanup [Boolean]
|
|
69
|
+
def initialize(io, original_filename:, cleanup:)
|
|
70
|
+
@io = io
|
|
71
|
+
@original_filename = original_filename
|
|
72
|
+
@cleanup = cleanup
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [String]
|
|
76
|
+
def path
|
|
77
|
+
io.path
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [Integer]
|
|
81
|
+
def size
|
|
82
|
+
return io.size if io.respond_to?(:size)
|
|
83
|
+
|
|
84
|
+
::File.size(path)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [String, nil]
|
|
88
|
+
def read(*args)
|
|
89
|
+
io.read(*args)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Integer]
|
|
93
|
+
def seek(*args)
|
|
94
|
+
io.seek(*args)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @return [void]
|
|
98
|
+
def rewind
|
|
99
|
+
io.rewind
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Close and unlink the wrapped tempfile when cleanup is enabled.
|
|
103
|
+
#
|
|
104
|
+
# @return [void]
|
|
105
|
+
def close!
|
|
106
|
+
return unless @cleanup
|
|
107
|
+
|
|
108
|
+
io.close!
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Generates upload parameters for Upload API requests.
|
|
4
|
+
#
|
|
5
|
+
# Builds the parameter hash needed for file uploads, including public key,
|
|
6
|
+
# store preferences, metadata, and optional signature params.
|
|
7
|
+
class Uploadcare::Internal::UploadParamsGenerator
|
|
8
|
+
class << self
|
|
9
|
+
# Build upload parameters.
|
|
10
|
+
#
|
|
11
|
+
# @param options [Hash] Upload options (:store, :metadata, :signature, :expire)
|
|
12
|
+
# @param config [Uploadcare::Configuration] Configuration with public key and signing settings
|
|
13
|
+
# @return [Hash] Upload parameters hash
|
|
14
|
+
def call(options: {}, config: Uploadcare.configuration)
|
|
15
|
+
params = {
|
|
16
|
+
'UPLOADCARE_PUB_KEY' => config.public_key
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
store = store_value(options[:store])
|
|
20
|
+
params['UPLOADCARE_STORE'] = store unless store.nil?
|
|
21
|
+
|
|
22
|
+
params.merge!(metadata(options: options))
|
|
23
|
+
params.merge!(signature_params(options: options, config: config))
|
|
24
|
+
|
|
25
|
+
params.compact
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Convert store option to API format.
|
|
31
|
+
#
|
|
32
|
+
# @param store [Boolean, String, Integer, nil] Store option value
|
|
33
|
+
# @return [String, nil] Formatted store value ('0', '1', or nil)
|
|
34
|
+
def store_value(store)
|
|
35
|
+
return nil if store.nil?
|
|
36
|
+
|
|
37
|
+
case store
|
|
38
|
+
when true, '1', 1 then '1'
|
|
39
|
+
when false, '0', 0 then '0'
|
|
40
|
+
else store.to_s
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Generate metadata parameters from options hash.
|
|
45
|
+
#
|
|
46
|
+
# @param options [Hash] Options containing :metadata key
|
|
47
|
+
# @return [Hash] Metadata params formatted as "metadata[key]" => "value"
|
|
48
|
+
def metadata(options:)
|
|
49
|
+
return {} if options[:metadata].nil?
|
|
50
|
+
raise ArgumentError, 'metadata must be a hash' unless options[:metadata].is_a?(Hash)
|
|
51
|
+
|
|
52
|
+
options[:metadata].each_with_object({}) do |(k, v), res|
|
|
53
|
+
res["metadata[#{k}]"] = v.to_s
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Generate signature parameters for signed uploads.
|
|
58
|
+
#
|
|
59
|
+
# @param options [Hash] Options with optional :signature and :expire keys
|
|
60
|
+
# @param config [Uploadcare::Configuration] Configuration with signing settings
|
|
61
|
+
# @return [Hash] Signature parameters
|
|
62
|
+
def signature_params(options:, config:)
|
|
63
|
+
return explicit_signature_params(options) if options.key?(:signature)
|
|
64
|
+
return {} unless config.sign_uploads
|
|
65
|
+
|
|
66
|
+
signature_data = Uploadcare::Internal::SignatureGenerator.call(config: config)
|
|
67
|
+
return { 'signature' => signature_data } unless signature_data.is_a?(Hash)
|
|
68
|
+
|
|
69
|
+
params = {}
|
|
70
|
+
params['signature'] = signature_data[:signature] || signature_data['signature']
|
|
71
|
+
params['expire'] = signature_data[:expire] || signature_data['expire']
|
|
72
|
+
params.compact
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Extract explicit signature params from options.
|
|
76
|
+
#
|
|
77
|
+
# @param options [Hash] Options with :signature and optional :expire
|
|
78
|
+
# @return [Hash] Signature parameters
|
|
79
|
+
def explicit_signature_params(options)
|
|
80
|
+
params = {}
|
|
81
|
+
params['signature'] = options[:signature]
|
|
82
|
+
params['expire'] = options[:expire] if options.key?(:expire)
|
|
83
|
+
params.compact
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# User-Agent string builder for Uploadcare API requests.
|
|
4
|
+
#
|
|
5
|
+
# Generates a standardized User-Agent header that identifies the Ruby gem version,
|
|
6
|
+
# public key, Ruby version, and optional framework information.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# Uploadcare::Internal::UserAgent.call(config: config)
|
|
10
|
+
# # => "UploadcareRuby/5.0.0.rc1/demopublickey (Ruby/3.3.0)"
|
|
11
|
+
class Uploadcare::Internal::UserAgent
|
|
12
|
+
# Build a User-Agent string.
|
|
13
|
+
#
|
|
14
|
+
# @param config [Uploadcare::Configuration] Configuration with public key and framework data
|
|
15
|
+
# @return [String] Formatted User-Agent string
|
|
16
|
+
def self.call(config: Uploadcare.configuration)
|
|
17
|
+
framework_data = config.framework_data.to_s
|
|
18
|
+
framework_suffix = framework_data.empty? ? '' : "; #{framework_data}"
|
|
19
|
+
public_key = config.public_key
|
|
20
|
+
"UploadcareRuby/#{Uploadcare::VERSION}/#{public_key} (Ruby/#{RUBY_VERSION}#{framework_suffix})"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mime/types'
|
|
4
|
+
|
|
5
|
+
# Handles the complete multipart upload workflow.
|
|
6
|
+
#
|
|
7
|
+
# Multipart upload is used for files larger than the multipart threshold (default: 100MB).
|
|
8
|
+
# The process:
|
|
9
|
+
# 1. Start upload → get UUID and presigned URLs
|
|
10
|
+
# 2. Upload file parts to presigned URLs (optionally in parallel)
|
|
11
|
+
# 3. Complete upload → finalize and get file info
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# mp = Uploadcare::Operations::MultipartUpload.new(upload_client: upload, config: config)
|
|
15
|
+
# result = mp.upload(file: large_file, store: true, threads: 4)
|
|
16
|
+
class Uploadcare::Operations::MultipartUpload
|
|
17
|
+
CHUNK_SIZE = 5_242_880 # 5MB default chunk size
|
|
18
|
+
|
|
19
|
+
# @return [Uploadcare::Api::Upload] Upload API client
|
|
20
|
+
attr_reader :upload_client
|
|
21
|
+
|
|
22
|
+
# @return [Uploadcare::Configuration] Configuration
|
|
23
|
+
attr_reader :config
|
|
24
|
+
|
|
25
|
+
# @param upload_client [Uploadcare::Api::Upload] Upload API client
|
|
26
|
+
# @param config [Uploadcare::Configuration] Configuration
|
|
27
|
+
def initialize(upload_client:, config:)
|
|
28
|
+
@upload_client = upload_client
|
|
29
|
+
@config = config
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Execute the full multipart upload flow.
|
|
33
|
+
#
|
|
34
|
+
# @param file [File, IO] File to upload
|
|
35
|
+
# @param options [Hash] Upload options (:store, :metadata, :threads, :part_size)
|
|
36
|
+
# @param request_options [Hash] Request options
|
|
37
|
+
# @yield [Hash] Progress callback with :uploaded, :total, :part, :total_parts
|
|
38
|
+
# @return [Uploadcare::Result] Result containing { 'uuid' => '...' }
|
|
39
|
+
def upload(file:, request_options: {}, **options, &block)
|
|
40
|
+
Uploadcare::Result.capture do
|
|
41
|
+
prepared_file = Uploadcare::Internal::UploadIo.wrap(file)
|
|
42
|
+
file_size = prepared_file.size
|
|
43
|
+
filename = prepared_file.original_filename
|
|
44
|
+
content_type = MIME::Types.type_for(prepared_file.path).first&.content_type || 'application/octet-stream'
|
|
45
|
+
part_size, threads = normalize_upload_options(options)
|
|
46
|
+
|
|
47
|
+
start_response = Uploadcare::Result.unwrap(
|
|
48
|
+
upload_client.files.multipart_start(
|
|
49
|
+
filename: filename,
|
|
50
|
+
size: file_size,
|
|
51
|
+
content_type: content_type,
|
|
52
|
+
request_options: request_options,
|
|
53
|
+
**options
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
uuid = start_response['uuid']
|
|
58
|
+
presigned_urls = start_response['parts']
|
|
59
|
+
|
|
60
|
+
if threads > 1
|
|
61
|
+
upload_parts_parallel(prepared_file, presigned_urls, part_size, threads, &block)
|
|
62
|
+
else
|
|
63
|
+
upload_parts_sequential(prepared_file, presigned_urls, part_size, &block)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
Uploadcare::Result.unwrap(
|
|
67
|
+
upload_client.files.multipart_complete(uuid: uuid, request_options: request_options)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
{ 'uuid' => uuid }
|
|
71
|
+
ensure
|
|
72
|
+
prepared_file&.close!
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def normalize_upload_options(options)
|
|
79
|
+
part_size = Integer(options.fetch(:part_size, config.multipart_chunk_size || CHUNK_SIZE))
|
|
80
|
+
max_threads = Integer(config.upload_threads || 1)
|
|
81
|
+
threads = Integer(options.fetch(:threads, max_threads))
|
|
82
|
+
|
|
83
|
+
raise ArgumentError, 'part_size must be > 0' if part_size <= 0
|
|
84
|
+
raise ArgumentError, 'upload_threads must be >= 1' if max_threads < 1
|
|
85
|
+
raise ArgumentError, 'threads must be >= 1' if threads < 1
|
|
86
|
+
raise ArgumentError, "threads must be <= #{max_threads}" if threads > max_threads
|
|
87
|
+
|
|
88
|
+
[part_size, threads]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def upload_parts_sequential(file, presigned_urls, part_size, &block)
|
|
92
|
+
total_size = file.respond_to?(:size) ? file.size : ::File.size(file.path)
|
|
93
|
+
uploaded = 0
|
|
94
|
+
|
|
95
|
+
presigned_urls.each_with_index do |presigned_url, index|
|
|
96
|
+
file.seek(index * part_size)
|
|
97
|
+
part_data = file.read(part_size)
|
|
98
|
+
break if part_data.nil? || part_data.empty?
|
|
99
|
+
|
|
100
|
+
upload_part(presigned_url, part_data)
|
|
101
|
+
uploaded += part_data.bytesize
|
|
102
|
+
|
|
103
|
+
block&.call(uploaded: uploaded, total: total_size, part: index + 1, total_parts: presigned_urls.length)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def upload_parts_parallel(file, presigned_urls, part_size, threads, &)
|
|
108
|
+
total_size = file.respond_to?(:size) ? file.size : ::File.size(file.path)
|
|
109
|
+
uploaded = { value: 0 }
|
|
110
|
+
mutex = Mutex.new
|
|
111
|
+
queue = Queue.new
|
|
112
|
+
errors = []
|
|
113
|
+
cancel = { value: false }
|
|
114
|
+
file_path = file.path
|
|
115
|
+
total_parts = presigned_urls.length
|
|
116
|
+
|
|
117
|
+
presigned_urls.each_with_index { |url, index| queue << [url, index] }
|
|
118
|
+
threads.times { queue << nil }
|
|
119
|
+
|
|
120
|
+
worker_context = {
|
|
121
|
+
queue: queue,
|
|
122
|
+
file_path: file_path,
|
|
123
|
+
part_size: part_size,
|
|
124
|
+
total_size: total_size,
|
|
125
|
+
total_parts: total_parts,
|
|
126
|
+
mutex: mutex,
|
|
127
|
+
uploaded: uploaded,
|
|
128
|
+
errors: errors,
|
|
129
|
+
cancel: cancel
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
workers = threads.times.map do
|
|
133
|
+
Thread.new do
|
|
134
|
+
run_parallel_worker(worker_context, &)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
workers.each(&:join)
|
|
139
|
+
raise errors.first if errors.any?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def run_parallel_worker(context, &)
|
|
143
|
+
::File.open(context[:file_path], 'rb') do |worker_file|
|
|
144
|
+
process_parallel_jobs(worker_file, context, &)
|
|
145
|
+
rescue StandardError => e
|
|
146
|
+
record_parallel_error(context, e)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def process_parallel_jobs(worker_file, context, &)
|
|
151
|
+
loop do
|
|
152
|
+
job = context[:queue].pop
|
|
153
|
+
break unless process_parallel_job?(worker_file, context, job, &)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def process_parallel_job?(worker_file, context, job)
|
|
158
|
+
return false if job.nil? || context[:cancel][:value]
|
|
159
|
+
|
|
160
|
+
presigned_url, index = job
|
|
161
|
+
offset = index * context[:part_size]
|
|
162
|
+
return false if offset >= context[:total_size]
|
|
163
|
+
|
|
164
|
+
worker_file.seek(offset)
|
|
165
|
+
part_data = worker_file.read(context[:part_size])
|
|
166
|
+
return false if part_data.nil? || part_data.empty?
|
|
167
|
+
|
|
168
|
+
upload_part(presigned_url, part_data)
|
|
169
|
+
update_parallel_progress(context, index, part_data.bytesize) { |progress| yield(progress) if block_given? }
|
|
170
|
+
true
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def update_parallel_progress(context, index, bytesize)
|
|
174
|
+
context[:mutex].synchronize do
|
|
175
|
+
context[:uploaded][:value] += bytesize
|
|
176
|
+
progress = {
|
|
177
|
+
uploaded: context[:uploaded][:value],
|
|
178
|
+
total: context[:total_size],
|
|
179
|
+
part: index + 1,
|
|
180
|
+
total_parts: context[:total_parts]
|
|
181
|
+
}
|
|
182
|
+
yield(progress)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def record_parallel_error(context, error)
|
|
187
|
+
context[:mutex].synchronize do
|
|
188
|
+
context[:cancel][:value] = true
|
|
189
|
+
context[:errors] << error
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def upload_part(presigned_url, part_data)
|
|
194
|
+
upload_client.upload_part_to_url(
|
|
195
|
+
presigned_url,
|
|
196
|
+
part_data,
|
|
197
|
+
max_retries: configured_max_upload_retries,
|
|
198
|
+
timeout: configured_upload_timeout
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def configured_max_upload_retries
|
|
203
|
+
value = config.max_upload_retries
|
|
204
|
+
value.nil? ? 3 : Integer(value)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def configured_upload_timeout
|
|
208
|
+
value = config.upload_timeout
|
|
209
|
+
return nil if value.nil?
|
|
210
|
+
|
|
211
|
+
Integer(value)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Routes upload requests to the appropriate Upload API endpoint.
|
|
4
|
+
#
|
|
5
|
+
# Handles the decision logic for choosing between:
|
|
6
|
+
# - Direct upload (small files < multipart threshold)
|
|
7
|
+
# - Multipart upload (large files >= multipart threshold)
|
|
8
|
+
# - URL upload (string URLs)
|
|
9
|
+
# - Batch upload (arrays of files)
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# router = Uploadcare::Operations::UploadRouter.new(client: client)
|
|
13
|
+
# file = router.upload(File.open("image.jpg"))
|
|
14
|
+
# file = router.upload("https://example.com/image.jpg")
|
|
15
|
+
# files = router.upload([file1, file2])
|
|
16
|
+
class Uploadcare::Operations::UploadRouter
|
|
17
|
+
# @return [Uploadcare::Client] Client instance
|
|
18
|
+
attr_reader :client
|
|
19
|
+
|
|
20
|
+
# @param client [Uploadcare::Client] Client instance
|
|
21
|
+
def initialize(client:)
|
|
22
|
+
@client = client
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Upload a file, URL, or array of files.
|
|
26
|
+
#
|
|
27
|
+
# Automatically routes to the appropriate upload method based on the source type:
|
|
28
|
+
# - File/IO objects >= multipart threshold → multipart upload
|
|
29
|
+
# - File/IO objects < multipart threshold → direct upload
|
|
30
|
+
# - Arrays → batch direct upload
|
|
31
|
+
# - Strings → URL upload
|
|
32
|
+
#
|
|
33
|
+
# @param source [File, IO, String, Array] Upload source
|
|
34
|
+
# @param options [Hash] Upload options (:store, :metadata, etc.)
|
|
35
|
+
# @param request_options [Hash] Request options
|
|
36
|
+
# @return [Uploadcare::Resources::File, Array<Uploadcare::Resources::File>, Hash]
|
|
37
|
+
# @raise [ArgumentError] if source type is not recognized
|
|
38
|
+
def upload(source, request_options: {}, **options, &block)
|
|
39
|
+
if big_file?(source)
|
|
40
|
+
multipart_upload(file: source, request_options: request_options, **options, &block)
|
|
41
|
+
elsif file?(source)
|
|
42
|
+
upload_file(file: source, request_options: request_options, **options)
|
|
43
|
+
elsif source.is_a?(Array)
|
|
44
|
+
upload_files(files: source, request_options: request_options, **options)
|
|
45
|
+
elsif source.is_a?(String)
|
|
46
|
+
upload_from_url(url: source, request_options: request_options, **options)
|
|
47
|
+
else
|
|
48
|
+
raise ArgumentError, "Expected input to be a File/Array/URL, given: `#{source}`"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Upload a single file directly.
|
|
53
|
+
#
|
|
54
|
+
# @param file [File, IO] File to upload
|
|
55
|
+
# @param options [Hash] Upload options
|
|
56
|
+
# @param request_options [Hash] Request options
|
|
57
|
+
# @return [Uploadcare::Resources::File]
|
|
58
|
+
def upload_file(file:, request_options: {}, **options)
|
|
59
|
+
response = Uploadcare::Result.unwrap(
|
|
60
|
+
client.api.upload.files.direct_many(files: [file], request_options: request_options, **options)
|
|
61
|
+
)
|
|
62
|
+
file_name, uuid = response.first
|
|
63
|
+
Uploadcare::Resources::File.new({ uuid: uuid, original_filename: decode_uploaded_filename(file_name) }, client)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Upload multiple files directly.
|
|
67
|
+
#
|
|
68
|
+
# @param files [Array<File, IO>] Files to upload
|
|
69
|
+
# @param options [Hash] Upload options
|
|
70
|
+
# @param request_options [Hash] Request options
|
|
71
|
+
# @return [Array<Uploadcare::Resources::File>]
|
|
72
|
+
def upload_files(files:, request_options: {}, **options)
|
|
73
|
+
response = Uploadcare::Result.unwrap(
|
|
74
|
+
client.api.upload.files.direct_many(files: files, request_options: request_options, **options)
|
|
75
|
+
)
|
|
76
|
+
response.map do |file_name, uuid|
|
|
77
|
+
Uploadcare::Resources::File.new({ uuid: uuid, original_filename: decode_uploaded_filename(file_name) }, client)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Upload a file from URL.
|
|
82
|
+
#
|
|
83
|
+
# @param url [String] Source URL
|
|
84
|
+
# @param options [Hash] Upload options (:async, :store, :metadata)
|
|
85
|
+
# @param request_options [Hash] Request options
|
|
86
|
+
# @return [Uploadcare::Resources::File, Hash] File resource (sync) or token hash (async)
|
|
87
|
+
def upload_from_url(url:, request_options: {}, **options)
|
|
88
|
+
response = Uploadcare::Result.unwrap(
|
|
89
|
+
client.api.upload.files.from_url(source_url: url, request_options: request_options, **options)
|
|
90
|
+
)
|
|
91
|
+
return response if options[:async]
|
|
92
|
+
|
|
93
|
+
Uploadcare::Resources::File.new(response, client)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Upload a large file using multipart upload.
|
|
97
|
+
#
|
|
98
|
+
# @param file [File, IO] Large file to upload
|
|
99
|
+
# @param options [Hash] Upload options
|
|
100
|
+
# @param request_options [Hash] Request options
|
|
101
|
+
# @return [Uploadcare::Resources::File]
|
|
102
|
+
def multipart_upload(file:, request_options: {}, **options, &block)
|
|
103
|
+
response = Uploadcare::Result.unwrap(
|
|
104
|
+
Uploadcare::Operations::MultipartUpload.new(
|
|
105
|
+
upload_client: client.api.upload,
|
|
106
|
+
config: client.config
|
|
107
|
+
).upload(file: file, request_options: request_options, **options, &block)
|
|
108
|
+
)
|
|
109
|
+
return response unless response.is_a?(Hash) && response['uuid']
|
|
110
|
+
|
|
111
|
+
Uploadcare::Resources::File.new(response, client)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get upload-from-URL status.
|
|
115
|
+
#
|
|
116
|
+
# @param token [String] Upload token
|
|
117
|
+
# @param request_options [Hash] Request options
|
|
118
|
+
# @return [Hash] Status response
|
|
119
|
+
def upload_from_url_status(token:, request_options: {})
|
|
120
|
+
Uploadcare::Result.unwrap(
|
|
121
|
+
client.api.upload.files.from_url_status(token: token, request_options: request_options)
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get file info from Upload API (without secret key).
|
|
126
|
+
#
|
|
127
|
+
# @param file_id [String] File UUID
|
|
128
|
+
# @param request_options [Hash] Request options
|
|
129
|
+
# @return [Hash] File information
|
|
130
|
+
def file_info(file_id:, request_options: {})
|
|
131
|
+
Uploadcare::Result.unwrap(
|
|
132
|
+
client.api.upload.files.info(file_id: file_id, request_options: request_options)
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def file?(object)
|
|
139
|
+
!object.is_a?(String) && object.respond_to?(:read)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def big_file?(object)
|
|
143
|
+
return false unless file?(object)
|
|
144
|
+
|
|
145
|
+
upload_size(object) >= client.config.multipart_size_threshold
|
|
146
|
+
rescue StandardError
|
|
147
|
+
false
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def upload_size(object)
|
|
151
|
+
return object.size if object.respond_to?(:size)
|
|
152
|
+
return ::File.size(object.path) if object.respond_to?(:path) && object.path && ::File.exist?(object.path)
|
|
153
|
+
|
|
154
|
+
0
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def decode_uploaded_filename(file_name)
|
|
158
|
+
encoded_prefix = /\A__uploadcare_form_\d+(?:_\d+)?__(.+)\z/
|
|
159
|
+
match = encoded_prefix.match(file_name.to_s)
|
|
160
|
+
match ? match[1] : file_name
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Add-on execution resource.
|
|
4
|
+
#
|
|
5
|
+
# Provides a unified interface for executing and checking status of
|
|
6
|
+
# AWS Rekognition, ClamAV, and Remove.bg add-ons.
|
|
7
|
+
#
|
|
8
|
+
# @see https://uploadcare.com/api-refs/rest-api/v0.7.0/#tag/Add-Ons
|
|
9
|
+
class Uploadcare::Resources::AddonExecution < Uploadcare::Resources::BaseResource
|
|
10
|
+
attr_accessor :request_id, :status, :result
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Execute AWS Rekognition label detection.
|
|
14
|
+
def aws_rekognition_detect_labels(uuid:, client: nil, config: Uploadcare.configuration, request_options: {})
|
|
15
|
+
execute_addon(:aws_rekognition_detect_labels, client: client, config: config,
|
|
16
|
+
request_options: request_options, uuid: uuid)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Check AWS Rekognition label detection status.
|
|
20
|
+
def aws_rekognition_detect_labels_status(request_id:, client: nil, config: Uploadcare.configuration,
|
|
21
|
+
request_options: {})
|
|
22
|
+
check_addon_status(:aws_rekognition_detect_labels_status, client: client, config: config,
|
|
23
|
+
request_options: request_options,
|
|
24
|
+
request_id: request_id)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Execute AWS Rekognition moderation label detection.
|
|
28
|
+
def aws_rekognition_detect_moderation_labels(uuid:, client: nil, config: Uploadcare.configuration,
|
|
29
|
+
request_options: {})
|
|
30
|
+
execute_addon(:aws_rekognition_detect_moderation_labels, client: client, config: config,
|
|
31
|
+
request_options: request_options, uuid: uuid)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Check AWS Rekognition moderation label detection status.
|
|
35
|
+
def aws_rekognition_detect_moderation_labels_status(request_id:, client: nil,
|
|
36
|
+
config: Uploadcare.configuration, request_options: {})
|
|
37
|
+
check_addon_status(:aws_rekognition_detect_moderation_labels_status, client: client, config: config,
|
|
38
|
+
request_options: request_options,
|
|
39
|
+
request_id: request_id)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Execute ClamAV virus scan.
|
|
43
|
+
def uc_clamav_virus_scan(uuid:, params: {}, client: nil, config: Uploadcare.configuration, request_options: {})
|
|
44
|
+
resolved_client = resolve_client(client: client, config: config)
|
|
45
|
+
response = Uploadcare::Result.unwrap(
|
|
46
|
+
resolved_client.api.rest.addons.uc_clamav_virus_scan(
|
|
47
|
+
uuid: uuid, params: params, request_options: request_options
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
new(response, resolved_client)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check ClamAV virus scan status.
|
|
54
|
+
def uc_clamav_virus_scan_status(request_id:, client: nil, config: Uploadcare.configuration,
|
|
55
|
+
request_options: {})
|
|
56
|
+
check_addon_status(:uc_clamav_virus_scan_status, client: client, config: config,
|
|
57
|
+
request_options: request_options, request_id: request_id)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Execute Remove.bg background removal.
|
|
61
|
+
def remove_bg(uuid:, params: {}, client: nil, config: Uploadcare.configuration, request_options: {})
|
|
62
|
+
resolved_client = resolve_client(client: client, config: config)
|
|
63
|
+
response = Uploadcare::Result.unwrap(
|
|
64
|
+
resolved_client.api.rest.addons.remove_bg(
|
|
65
|
+
uuid: uuid, params: params, request_options: request_options
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
new(response, resolved_client)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Check Remove.bg execution status.
|
|
72
|
+
def remove_bg_status(request_id:, client: nil, config: Uploadcare.configuration, request_options: {})
|
|
73
|
+
check_addon_status(:remove_bg_status, client: client, config: config,
|
|
74
|
+
request_options: request_options, request_id: request_id)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def execute_addon(method_name, client:, config:, request_options:, uuid:)
|
|
80
|
+
resolved_client = resolve_client(client: client, config: config)
|
|
81
|
+
response = Uploadcare::Result.unwrap(
|
|
82
|
+
resolved_client.api.rest.addons.public_send(method_name, uuid: uuid, request_options: request_options)
|
|
83
|
+
)
|
|
84
|
+
new(response, resolved_client)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def check_addon_status(method_name, client:, config:, request_options:, request_id:)
|
|
88
|
+
resolved_client = resolve_client(client: client, config: config)
|
|
89
|
+
response = Uploadcare::Result.unwrap(
|
|
90
|
+
resolved_client.api.rest.addons.public_send(
|
|
91
|
+
method_name, request_id: request_id, request_options: request_options
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
new(response, resolved_client)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|