uploadcare-ruby 5.0.0 → 5.1.0
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/.github/workflows/gem-push.yml +3 -1
- data/CHANGELOG.md +18 -0
- data/MIGRATING_V5.md +2 -1
- data/README.md +108 -6
- data/api_examples/README.md +9 -5
- data/api_examples/rest_api/get_files_uuid_tags.rb +4 -0
- data/api_examples/rest_api/patch_files_uuid_tags.rb +4 -0
- data/api_examples/rest_api/post_files_search.rb +4 -0
- data/api_examples/rest_api/put_files_uuid_tags.rb +4 -0
- data/api_examples/support/example_helper.rb +17 -1
- data/api_examples/support/run_rest_example.rb +30 -0
- data/api_examples/support/run_upload_example.rb +8 -2
- data/context7.json +3 -1
- data/docs/release-notes-5.1.0.md +33 -0
- data/examples/README.md +4 -0
- data/examples/file_search.rb +33 -0
- data/examples/file_tags.rb +37 -0
- data/lib/uploadcare/api/rest/file_tags.rb +62 -0
- data/lib/uploadcare/api/rest/files.rb +18 -1
- data/lib/uploadcare/api/rest.rb +37 -12
- data/lib/uploadcare/api/upload/files.rb +18 -5
- data/lib/uploadcare/api/upload.rb +2 -1
- data/lib/uploadcare/client/file_tags_accessor.rb +44 -0
- data/lib/uploadcare/client/files_accessor.rb +17 -0
- data/lib/uploadcare/client.rb +7 -0
- data/lib/uploadcare/collections/file_search_result.rb +45 -0
- data/lib/uploadcare/collections/paginated.rb +7 -1
- data/lib/uploadcare/internal/file_tag_normalizer.rb +62 -0
- data/lib/uploadcare/internal/upload_params_generator.rb +16 -2
- data/lib/uploadcare/internal/user_agent.rb +1 -1
- data/lib/uploadcare/operations/file_search.rb +43 -0
- data/lib/uploadcare/operations/multipart_upload.rb +1 -1
- data/lib/uploadcare/operations/upload_router.rb +2 -2
- data/lib/uploadcare/resources/file.rb +23 -15
- data/lib/uploadcare/resources/file_tags.rb +92 -0
- data/lib/uploadcare/version.rb +1 -1
- data/lib/uploadcare.rb +2 -0
- metadata +14 -1
data/lib/uploadcare/api/rest.rb
CHANGED
|
@@ -5,7 +5,7 @@ require 'uri'
|
|
|
5
5
|
|
|
6
6
|
# Base client for the Uploadcare REST API.
|
|
7
7
|
#
|
|
8
|
-
# Provides authenticated HTTP methods (GET, POST, PUT, DELETE) for all REST API
|
|
8
|
+
# Provides authenticated HTTP methods (GET, POST, PUT, PATCH, DELETE) for all REST API
|
|
9
9
|
# endpoints. Includes automatic error handling and throttle retry logic.
|
|
10
10
|
#
|
|
11
11
|
# Endpoint classes are accessed via lazy-loaded accessors:
|
|
@@ -71,6 +71,11 @@ class Uploadcare::Api::Rest
|
|
|
71
71
|
memoized(:@file_metadata) { Uploadcare::Api::Rest::FileMetadata.new(rest: self) }
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
# @return [Uploadcare::Api::Rest::FileTags] Per-file tag operations endpoint
|
|
75
|
+
def file_tags
|
|
76
|
+
memoized(:@file_tags) { Uploadcare::Api::Rest::FileTags.new(rest: self) }
|
|
77
|
+
end
|
|
78
|
+
|
|
74
79
|
# @return [Uploadcare::Api::Rest::Addons] Add-on operations endpoint
|
|
75
80
|
def addons
|
|
76
81
|
memoized(:@addons) { Uploadcare::Api::Rest::Addons.new(rest: self) }
|
|
@@ -90,17 +95,18 @@ class Uploadcare::Api::Rest
|
|
|
90
95
|
|
|
91
96
|
# Make an HTTP request to the REST API.
|
|
92
97
|
#
|
|
93
|
-
# @param method [Symbol] HTTP method (:get, :post, :put, :delete)
|
|
98
|
+
# @param method [Symbol] HTTP method (:get, :post, :put, :patch, :delete)
|
|
94
99
|
# @param path [String] API endpoint path
|
|
95
100
|
# @param params [Hash, Array, String] Request parameters
|
|
101
|
+
# @param query [Hash] Query parameters for requests that also have a body
|
|
96
102
|
# @param headers [Hash] Additional request headers
|
|
97
103
|
# @param request_options [Hash] Request options (timeout, etc.)
|
|
98
104
|
# @return [Hash, Array, nil] Parsed JSON response body
|
|
99
105
|
# @raise [Uploadcare::Exception::RequestError] on API errors
|
|
100
|
-
def make_request(method:, path:, params: {}, headers: {}, request_options: {})
|
|
106
|
+
def make_request(method:, path:, params: {}, query: {}, headers: {}, request_options: {})
|
|
101
107
|
handle_throttling(max_attempts: request_options[:max_throttle_attempts]) do
|
|
102
108
|
response = connection.public_send(method, path) do |req|
|
|
103
|
-
prepare_request(req, method, path, params, headers, request_options)
|
|
109
|
+
prepare_request(req, method, path, params, query, headers, request_options)
|
|
104
110
|
end
|
|
105
111
|
response.body
|
|
106
112
|
end
|
|
@@ -112,11 +118,14 @@ class Uploadcare::Api::Rest
|
|
|
112
118
|
#
|
|
113
119
|
# @param path [String] API endpoint path
|
|
114
120
|
# @param params [Hash] Request body parameters
|
|
121
|
+
# @param query [Hash] Query parameters
|
|
115
122
|
# @param headers [Hash] Additional request headers
|
|
116
123
|
# @param request_options [Hash] Request options
|
|
117
124
|
# @return [Uploadcare::Result]
|
|
118
|
-
def post(path:, params: {}, headers: {}, request_options: {})
|
|
119
|
-
request(
|
|
125
|
+
def post(path:, params: {}, query: {}, headers: {}, request_options: {})
|
|
126
|
+
request(
|
|
127
|
+
method: :post, path: path, params: params, query: query, headers: headers, request_options: request_options
|
|
128
|
+
)
|
|
120
129
|
end
|
|
121
130
|
|
|
122
131
|
# Make a GET request wrapped in a Result.
|
|
@@ -141,6 +150,17 @@ class Uploadcare::Api::Rest
|
|
|
141
150
|
request(method: :put, path: path, params: params, headers: headers, request_options: request_options)
|
|
142
151
|
end
|
|
143
152
|
|
|
153
|
+
# Make a PATCH request wrapped in a Result.
|
|
154
|
+
#
|
|
155
|
+
# @param path [String] API endpoint path
|
|
156
|
+
# @param params [Hash] Request body parameters
|
|
157
|
+
# @param headers [Hash] Additional request headers
|
|
158
|
+
# @param request_options [Hash] Request options
|
|
159
|
+
# @return [Uploadcare::Result]
|
|
160
|
+
def patch(path:, params: {}, headers: {}, request_options: {})
|
|
161
|
+
request(method: :patch, path: path, params: params, headers: headers, request_options: request_options)
|
|
162
|
+
end
|
|
163
|
+
|
|
144
164
|
# Make a DELETE request wrapped in a Result.
|
|
145
165
|
#
|
|
146
166
|
# @param path [String] API endpoint path
|
|
@@ -157,28 +177,33 @@ class Uploadcare::Api::Rest
|
|
|
157
177
|
# @param method [Symbol] HTTP method
|
|
158
178
|
# @param path [String] API path
|
|
159
179
|
# @param params [Hash] Request parameters
|
|
180
|
+
# @param query [Hash] Query parameters for requests that also have a body
|
|
160
181
|
# @param headers [Hash] Request headers
|
|
161
182
|
# @param request_options [Hash] Request options
|
|
162
183
|
# @return [Uploadcare::Result]
|
|
163
|
-
def request(method:, path:, params: {}, headers: {}, request_options: {})
|
|
184
|
+
def request(method:, path:, params: {}, query: {}, headers: {}, request_options: {})
|
|
164
185
|
Uploadcare::Result.capture do
|
|
165
|
-
make_request(
|
|
186
|
+
make_request(
|
|
187
|
+
method: method, path: path, params: params, query: query, headers: headers, request_options: request_options
|
|
188
|
+
)
|
|
166
189
|
end
|
|
167
190
|
end
|
|
168
191
|
|
|
169
192
|
private
|
|
170
193
|
|
|
171
|
-
def prepare_request(req, method, path, params, headers, request_options)
|
|
194
|
+
def prepare_request(req, method, path, params, query, headers, request_options)
|
|
172
195
|
upcase_method_name = method.to_s.upcase
|
|
173
|
-
|
|
196
|
+
query_params = upcase_method_name == HTTP_GET ? params : query
|
|
197
|
+
uri = build_request_uri(path, query_params)
|
|
174
198
|
|
|
175
199
|
prepare_headers(req, upcase_method_name, uri, params, headers)
|
|
176
200
|
prepare_body_or_params(req, upcase_method_name, params)
|
|
201
|
+
req.params.update(query) if upcase_method_name != HTTP_GET && !query.nil? && !query.empty?
|
|
177
202
|
apply_request_options(req, request_options)
|
|
178
203
|
end
|
|
179
204
|
|
|
180
|
-
def build_request_uri(path, params
|
|
181
|
-
if
|
|
205
|
+
def build_request_uri(path, params)
|
|
206
|
+
if !params.nil? && params.is_a?(Hash) && !params.empty?
|
|
182
207
|
build_uri(path, params)
|
|
183
208
|
else
|
|
184
209
|
path
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Upload API endpoint for file upload operations.
|
|
4
|
-
# rubocop:disable Metrics/ClassLength
|
|
4
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
5
5
|
class Uploadcare::Api::Upload::Files
|
|
6
6
|
# @return [Uploadcare::Api::Upload] Parent Upload client
|
|
7
7
|
attr_reader :upload
|
|
@@ -14,7 +14,7 @@ class Uploadcare::Api::Upload::Files
|
|
|
14
14
|
# Upload a file directly (POST /base/).
|
|
15
15
|
#
|
|
16
16
|
# @param file [File, IO] File object to upload
|
|
17
|
-
# @param options [Hash] Upload options (:store, :metadata, :signature, :expire)
|
|
17
|
+
# @param options [Hash] Upload options (:store, :metadata, :tags, :signature, :expire)
|
|
18
18
|
# @param request_options [Hash] Request options
|
|
19
19
|
# @return [Uploadcare::Result] Upload response with file UUID
|
|
20
20
|
# @raise [ArgumentError] if file is not a valid IO object
|
|
@@ -32,7 +32,7 @@ class Uploadcare::Api::Upload::Files
|
|
|
32
32
|
# Upload multiple files directly (POST /base/).
|
|
33
33
|
#
|
|
34
34
|
# @param files [Array<File, IO>] Files to upload
|
|
35
|
-
# @param options [Hash] Upload options (:store, :metadata)
|
|
35
|
+
# @param options [Hash] Upload options (:store, :metadata, :tags)
|
|
36
36
|
# @param request_options [Hash] Request options
|
|
37
37
|
# @return [Uploadcare::Result] Upload response hash mapping filenames to UUIDs
|
|
38
38
|
# @see https://uploadcare.com/api-refs/upload-api/#operation/baseUpload
|
|
@@ -63,6 +63,7 @@ class Uploadcare::Api::Upload::Files
|
|
|
63
63
|
# @option options [Boolean] :async Return immediately with token (default: false)
|
|
64
64
|
# @option options [String, Boolean] :store Whether to store the file
|
|
65
65
|
# @option options [Hash] :metadata Custom metadata
|
|
66
|
+
# @option options [Array<String>] :tags Tags to attach to the file
|
|
66
67
|
# @option options [Integer] :poll_interval Polling interval in seconds (default: 1)
|
|
67
68
|
# @option options [Integer] :poll_timeout Max polling time in seconds (default: 300)
|
|
68
69
|
# @param request_options [Hash] Request options
|
|
@@ -105,7 +106,7 @@ class Uploadcare::Api::Upload::Files
|
|
|
105
106
|
# @param filename [String] Original filename
|
|
106
107
|
# @param size [Integer] File size in bytes
|
|
107
108
|
# @param content_type [String] MIME type
|
|
108
|
-
# @param options [Hash] Upload options (:store, :metadata)
|
|
109
|
+
# @param options [Hash] Upload options (:store, :metadata, :tags)
|
|
109
110
|
# @param request_options [Hash] Request options
|
|
110
111
|
# @return [Uploadcare::Result] Response with UUID and presigned URLs
|
|
111
112
|
# @see https://uploadcare.com/api-refs/upload-api/#operation/multipartUploadStart
|
|
@@ -198,6 +199,8 @@ class Uploadcare::Api::Upload::Files
|
|
|
198
199
|
params['save_URL_duplicates'] = options[:save_URL_duplicates].to_s if options.key?(:save_URL_duplicates)
|
|
199
200
|
metadata_params = generate_metadata_params(options[:metadata])
|
|
200
201
|
params.merge!(metadata_params) if metadata_params.any?
|
|
202
|
+
tags_param = generate_tags_param(options[:tags])
|
|
203
|
+
params.merge!(tags_param) if tags_param.any?
|
|
201
204
|
params.merge!(signature_params(options))
|
|
202
205
|
params
|
|
203
206
|
end
|
|
@@ -213,6 +216,8 @@ class Uploadcare::Api::Upload::Files
|
|
|
213
216
|
params['UPLOADCARE_STORE'] = store unless store.nil?
|
|
214
217
|
metadata_params = generate_metadata_params(options[:metadata])
|
|
215
218
|
params.merge!(metadata_params) if metadata_params.any?
|
|
219
|
+
tags_param = generate_tags_param(options[:tags])
|
|
220
|
+
params.merge!(tags_param) if tags_param.any?
|
|
216
221
|
params.merge!(signature_params(options))
|
|
217
222
|
params
|
|
218
223
|
end
|
|
@@ -270,6 +275,15 @@ class Uploadcare::Api::Upload::Files
|
|
|
270
275
|
end
|
|
271
276
|
end
|
|
272
277
|
|
|
278
|
+
def generate_tags_param(tags = nil)
|
|
279
|
+
return {} if tags.nil?
|
|
280
|
+
|
|
281
|
+
normalized = Uploadcare::Internal::FileTagNormalizer.call(tags)
|
|
282
|
+
return {} if normalized.empty?
|
|
283
|
+
|
|
284
|
+
{ 'tags' => normalized.join(',') }
|
|
285
|
+
end
|
|
286
|
+
|
|
273
287
|
def signature_params(options = {})
|
|
274
288
|
return {} if options.nil?
|
|
275
289
|
|
|
@@ -310,4 +324,3 @@ class Uploadcare::Api::Upload::Files
|
|
|
310
324
|
[initial.to_f * (2**attempt), max_interval.to_f].min
|
|
311
325
|
end
|
|
312
326
|
end
|
|
313
|
-
# rubocop:enable Metrics/ClassLength
|
|
@@ -159,7 +159,7 @@ class Uploadcare::Api::Upload
|
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
def prepare_headers(req, _method, _uri, headers)
|
|
162
|
-
req.headers['User-Agent']
|
|
162
|
+
req.headers['User-Agent'] = Uploadcare::Internal::UserAgent.call(config: config)
|
|
163
163
|
req.headers.merge!(headers)
|
|
164
164
|
end
|
|
165
165
|
|
|
@@ -243,6 +243,7 @@ class Uploadcare::Api::Upload
|
|
|
243
243
|
def upload_part_request(conn:, request_uri:, data:, timeout:, open_timeout:)
|
|
244
244
|
conn.put(request_uri) do |req|
|
|
245
245
|
req.headers['Content-Type'] = 'application/octet-stream'
|
|
246
|
+
req.headers['User-Agent'] = Uploadcare::Internal::UserAgent.call(config: config)
|
|
246
247
|
req.options.timeout = timeout if timeout
|
|
247
248
|
req.options.open_timeout = open_timeout if open_timeout
|
|
248
249
|
req.body = data
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Per-file tag operations scoped to a client instance.
|
|
4
|
+
#
|
|
5
|
+
# @example Replace and update tags
|
|
6
|
+
# client.file_tags.replace(uuid: file.uuid, tags: %w[approved summer])
|
|
7
|
+
# client.file_tags.update(uuid: file.uuid, add: ["featured"], delete: ["summer"])
|
|
8
|
+
class Uploadcare::Client::FileTagsAccessor
|
|
9
|
+
attr_reader :client
|
|
10
|
+
|
|
11
|
+
# @param client [Uploadcare::Client]
|
|
12
|
+
def initialize(client:)
|
|
13
|
+
@client = client
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param uuid [String]
|
|
17
|
+
# @param request_options [Hash]
|
|
18
|
+
# @return [Array<String>]
|
|
19
|
+
def list(uuid:, request_options: {})
|
|
20
|
+
Uploadcare::Resources::FileTags.list(uuid: uuid, client: client, request_options: request_options)
|
|
21
|
+
end
|
|
22
|
+
alias index list
|
|
23
|
+
|
|
24
|
+
# @param uuid [String]
|
|
25
|
+
# @param tags [Array<String>]
|
|
26
|
+
# @param request_options [Hash]
|
|
27
|
+
# @return [Uploadcare::Resources::FileTags]
|
|
28
|
+
def replace(uuid:, tags:, request_options: {})
|
|
29
|
+
Uploadcare::Resources::FileTags.replace(
|
|
30
|
+
uuid: uuid, tags: tags, client: client, request_options: request_options
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param uuid [String]
|
|
35
|
+
# @param add [Array<String>]
|
|
36
|
+
# @param delete [Array<String>]
|
|
37
|
+
# @param request_options [Hash]
|
|
38
|
+
# @return [Uploadcare::Resources::FileTags]
|
|
39
|
+
def update(uuid:, add: [], delete: [], request_options: {})
|
|
40
|
+
Uploadcare::Resources::FileTags.update(
|
|
41
|
+
uuid: uuid, add: add, delete: delete, client: client, request_options: request_options
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -28,6 +28,23 @@ class Uploadcare::Client::FilesAccessor
|
|
|
28
28
|
)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Search files using full-text criteria and structured filters.
|
|
32
|
+
#
|
|
33
|
+
# @example Search PDFs and inspect highlighted matches
|
|
34
|
+
# results = client.files.search(
|
|
35
|
+
# query: "invoice", exact: { detected_mime_type: ["application/pdf"] }, limit: 20
|
|
36
|
+
# )
|
|
37
|
+
# results.each { |file| puts file.highlight }
|
|
38
|
+
#
|
|
39
|
+
# @param request_options [Hash]
|
|
40
|
+
# @param options [Hash] Search criteria plus limit, offset, and include
|
|
41
|
+
# @return [Uploadcare::Collections::FileSearchResult]
|
|
42
|
+
def search(request_options: {}, **options)
|
|
43
|
+
Uploadcare::Resources::File.search(
|
|
44
|
+
options: options, client: client, request_options: request_options
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
31
48
|
# @param source [IO, Array<IO>, String]
|
|
32
49
|
# @param request_options [Hash]
|
|
33
50
|
# @param options [Hash]
|
data/lib/uploadcare/client.rb
CHANGED
|
@@ -96,6 +96,13 @@ class Uploadcare::Client
|
|
|
96
96
|
memoized(:@file_metadata) { FileMetadataAccessor.new(client: self) }
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
# Access per-file tag operations.
|
|
100
|
+
#
|
|
101
|
+
# @return [Uploadcare::Client::FileTagsAccessor]
|
|
102
|
+
def file_tags
|
|
103
|
+
memoized(:@file_tags) { FileTagsAccessor.new(client: self) }
|
|
104
|
+
end
|
|
105
|
+
|
|
99
106
|
# Access conversion helpers.
|
|
100
107
|
#
|
|
101
108
|
# @return [Uploadcare::Client::ConversionsAccessor]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Paginated results returned by the file search endpoint.
|
|
4
|
+
#
|
|
5
|
+
# Search pagination uses POST requests, so every subsequent page must resend the
|
|
6
|
+
# original JSON search criteria while following the query parameters supplied by
|
|
7
|
+
# the API's next or previous URL.
|
|
8
|
+
class Uploadcare::Collections::FileSearchResult < Uploadcare::Collections::Paginated
|
|
9
|
+
# @return [Hash] JSON search criteria resent for subsequent pages
|
|
10
|
+
attr_reader :search_params, :search_query
|
|
11
|
+
|
|
12
|
+
def initialize(params = {})
|
|
13
|
+
@search_params = immutable_copy(params[:search_params] || {})
|
|
14
|
+
@search_query = immutable_copy(params[:search_query] || {})
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def immutable_copy(value)
|
|
21
|
+
case value
|
|
22
|
+
when Hash
|
|
23
|
+
value.each_with_object({}) do |(key, nested_value), copy|
|
|
24
|
+
copy[immutable_copy(key)] = immutable_copy(nested_value)
|
|
25
|
+
end.freeze
|
|
26
|
+
when Array
|
|
27
|
+
value.map { |nested_value| immutable_copy(nested_value) }.freeze
|
|
28
|
+
when String
|
|
29
|
+
value.dup.freeze
|
|
30
|
+
else
|
|
31
|
+
value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def fetch_response(params)
|
|
36
|
+
query = search_query.transform_keys(&:to_s).merge(params)
|
|
37
|
+
Uploadcare::Result.unwrap(
|
|
38
|
+
api_client.search(params: search_params, query: query, request_options: request_options)
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def continuation_options
|
|
43
|
+
{ search_params: search_params, search_query: search_query }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -155,10 +155,16 @@ class Uploadcare::Collections::Paginated
|
|
|
155
155
|
api_client: api_client,
|
|
156
156
|
resource_class: resource_class,
|
|
157
157
|
client: client,
|
|
158
|
-
request_options: request_options
|
|
158
|
+
request_options: request_options,
|
|
159
|
+
**continuation_options
|
|
159
160
|
)
|
|
160
161
|
end
|
|
161
162
|
|
|
163
|
+
# Extra state that a specialized collection needs to carry to subsequent pages.
|
|
164
|
+
def continuation_options
|
|
165
|
+
{}
|
|
166
|
+
end
|
|
167
|
+
|
|
162
168
|
def build_resources(results)
|
|
163
169
|
results.map { |data| resource_class.new(data, client) }
|
|
164
170
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Normalizes and validates file tags before sending them to Uploadcare.
|
|
4
|
+
class Uploadcare::Internal::FileTagNormalizer
|
|
5
|
+
MAX_LENGTH = 100
|
|
6
|
+
MAX_COUNT = 50
|
|
7
|
+
VALID_PATTERN = /\A[a-z0-9._-]+\z/
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
# Normalize a list of file tags.
|
|
11
|
+
#
|
|
12
|
+
# Tags are stripped, lowercased, and deduplicated while preserving their
|
|
13
|
+
# first-seen order.
|
|
14
|
+
#
|
|
15
|
+
# @param tags [Array<String>]
|
|
16
|
+
# @param max_count [Integer, nil] Maximum number of tags; nil disables the limit
|
|
17
|
+
# @return [Array<String>]
|
|
18
|
+
# @raise [ArgumentError] if a tag is invalid
|
|
19
|
+
def call(tags, max_count: MAX_COUNT)
|
|
20
|
+
raise ArgumentError, 'tags must be an array of strings' unless tags.is_a?(Array)
|
|
21
|
+
|
|
22
|
+
normalized = normalize(tags)
|
|
23
|
+
validate_count(normalized, max_count)
|
|
24
|
+
normalized
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def normalize(tags)
|
|
30
|
+
seen = {}
|
|
31
|
+
|
|
32
|
+
tags.each_with_object([]) do |tag, result|
|
|
33
|
+
raise ArgumentError, 'tags must be an array of strings' unless tag.is_a?(String)
|
|
34
|
+
|
|
35
|
+
value = tag.strip.downcase
|
|
36
|
+
next if value.empty?
|
|
37
|
+
|
|
38
|
+
validate_tag(value)
|
|
39
|
+
next if seen[value]
|
|
40
|
+
|
|
41
|
+
seen[value] = true
|
|
42
|
+
result << value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate_tag(tag)
|
|
47
|
+
if tag.length > MAX_LENGTH
|
|
48
|
+
raise ArgumentError, "tag is too long: #{tag.length} characters (maximum #{MAX_LENGTH})"
|
|
49
|
+
end
|
|
50
|
+
return if VALID_PATTERN.match?(tag)
|
|
51
|
+
|
|
52
|
+
raise ArgumentError,
|
|
53
|
+
'tag contains invalid characters; allowed: Latin letters, digits, hyphen, underscore, dot'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def validate_count(tags, max_count)
|
|
57
|
+
return if max_count.nil? || max_count.zero? || tags.length <= max_count
|
|
58
|
+
|
|
59
|
+
raise ArgumentError, "too many tags: #{tags.length} (maximum #{max_count})"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
# Generates upload parameters for Upload API requests.
|
|
4
4
|
#
|
|
5
5
|
# Builds the parameter hash needed for file uploads, including public key,
|
|
6
|
-
# store preferences, metadata, and optional signature params.
|
|
6
|
+
# store preferences, metadata, tags, and optional signature params.
|
|
7
7
|
class Uploadcare::Internal::UploadParamsGenerator
|
|
8
8
|
class << self
|
|
9
9
|
# Build upload parameters.
|
|
10
10
|
#
|
|
11
|
-
# @param options [Hash] Upload options (:store, :metadata, :signature, :expire)
|
|
11
|
+
# @param options [Hash] Upload options (:store, :metadata, :tags, :signature, :expire)
|
|
12
12
|
# @param config [Uploadcare::Configuration] Configuration with public key and signing settings
|
|
13
13
|
# @return [Hash] Upload parameters hash
|
|
14
14
|
def call(options: {}, config: Uploadcare.configuration)
|
|
@@ -20,6 +20,7 @@ class Uploadcare::Internal::UploadParamsGenerator
|
|
|
20
20
|
params['UPLOADCARE_STORE'] = store unless store.nil?
|
|
21
21
|
|
|
22
22
|
params.merge!(metadata(options: options))
|
|
23
|
+
params.merge!(tags(options: options))
|
|
23
24
|
params.merge!(signature_params(options: options, config: config))
|
|
24
25
|
|
|
25
26
|
params.compact
|
|
@@ -54,6 +55,19 @@ class Uploadcare::Internal::UploadParamsGenerator
|
|
|
54
55
|
end
|
|
55
56
|
end
|
|
56
57
|
|
|
58
|
+
# Generate the comma-separated tags parameter.
|
|
59
|
+
#
|
|
60
|
+
# @param options [Hash] Options containing :tags
|
|
61
|
+
# @return [Hash]
|
|
62
|
+
def tags(options:)
|
|
63
|
+
return {} if options[:tags].nil?
|
|
64
|
+
|
|
65
|
+
normalized = Uploadcare::Internal::FileTagNormalizer.call(options[:tags])
|
|
66
|
+
return {} if normalized.empty?
|
|
67
|
+
|
|
68
|
+
{ 'tags' => normalized.join(',') }
|
|
69
|
+
end
|
|
70
|
+
|
|
57
71
|
# Generate signature parameters for signed uploads.
|
|
58
72
|
#
|
|
59
73
|
# @param options [Hash] Options with optional :signature and :expire keys
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
#
|
|
8
8
|
# @example
|
|
9
9
|
# Uploadcare::Internal::UserAgent.call(config: config)
|
|
10
|
-
# # => "UploadcareRuby/5.
|
|
10
|
+
# # => "UploadcareRuby/5.1.0/demopublickey (Ruby/3.3.0)"
|
|
11
11
|
class Uploadcare::Internal::UserAgent
|
|
12
12
|
# Build a User-Agent string.
|
|
13
13
|
#
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Executes a file search and builds its POST-aware paginated result.
|
|
4
|
+
class Uploadcare::Operations::FileSearch
|
|
5
|
+
QUERY_OPTIONS = %i[limit offset include].freeze
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def call(options:, client:, resource_class:, request_options: {})
|
|
9
|
+
search_params, query_params = split_options(options)
|
|
10
|
+
response = Uploadcare::Result.unwrap(
|
|
11
|
+
client.api.rest.files.search(
|
|
12
|
+
params: search_params, query: query_params, request_options: request_options
|
|
13
|
+
)
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
Uploadcare::Collections::FileSearchResult.new(
|
|
17
|
+
resources: response.fetch('results', []).map { |data| resource_class.new(data, client) },
|
|
18
|
+
next_page: response['next'],
|
|
19
|
+
previous_page: response['previous'],
|
|
20
|
+
per_page: response['per_page'],
|
|
21
|
+
total: response['total'],
|
|
22
|
+
api_client: client.api.rest.files,
|
|
23
|
+
resource_class: resource_class,
|
|
24
|
+
client: client,
|
|
25
|
+
request_options: request_options,
|
|
26
|
+
search_params: search_params,
|
|
27
|
+
search_query: query_params
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def split_options(options)
|
|
34
|
+
body = options.dup
|
|
35
|
+
query = QUERY_OPTIONS.to_h do |key|
|
|
36
|
+
value = body.key?(key) ? body.delete(key) : body[key.to_s]
|
|
37
|
+
body.delete(key.to_s)
|
|
38
|
+
[key, value]
|
|
39
|
+
end
|
|
40
|
+
[body, query.compact]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -32,7 +32,7 @@ class Uploadcare::Operations::MultipartUpload
|
|
|
32
32
|
# Execute the full multipart upload flow.
|
|
33
33
|
#
|
|
34
34
|
# @param file [File, IO] File to upload
|
|
35
|
-
# @param options [Hash] Upload options (:store, :metadata, :threads, :part_size)
|
|
35
|
+
# @param options [Hash] Upload options (:store, :metadata, :tags, :threads, :part_size)
|
|
36
36
|
# @param request_options [Hash] Request options
|
|
37
37
|
# @yield [Hash] Progress callback with :uploaded, :total, :part, :total_parts
|
|
38
38
|
# @return [Uploadcare::Result] Result containing { 'uuid' => '...' }
|
|
@@ -31,7 +31,7 @@ class Uploadcare::Operations::UploadRouter
|
|
|
31
31
|
# - Strings → URL upload
|
|
32
32
|
#
|
|
33
33
|
# @param source [File, IO, String, Array] Upload source
|
|
34
|
-
# @param options [Hash] Upload options (:store, :metadata, etc.)
|
|
34
|
+
# @param options [Hash] Upload options (:store, :metadata, :tags, etc.)
|
|
35
35
|
# @param request_options [Hash] Request options
|
|
36
36
|
# @return [Uploadcare::Resources::File, Array<Uploadcare::Resources::File>, Hash]
|
|
37
37
|
# @raise [ArgumentError] if source type is not recognized
|
|
@@ -81,7 +81,7 @@ class Uploadcare::Operations::UploadRouter
|
|
|
81
81
|
# Upload a file from URL.
|
|
82
82
|
#
|
|
83
83
|
# @param url [String] Source URL
|
|
84
|
-
# @param options [Hash] Upload options (:async, :store, :metadata)
|
|
84
|
+
# @param options [Hash] Upload options (:async, :store, :metadata, :tags)
|
|
85
85
|
# @param request_options [Hash] Request options
|
|
86
86
|
# @return [Uploadcare::Resources::File, Hash] File resource (sync) or token hash (async)
|
|
87
87
|
def upload_from_url(url:, request_options: {}, **options)
|
|
@@ -19,13 +19,13 @@ class Uploadcare::Resources::File < Uploadcare::Resources::BaseResource
|
|
|
19
19
|
# API fields assigned onto file resources.
|
|
20
20
|
ATTRIBUTES = %i[
|
|
21
21
|
datetime_removed datetime_stored datetime_uploaded is_image is_ready mime_type original_file_url
|
|
22
|
-
original_filename size url uuid variations content_info metadata appdata source
|
|
22
|
+
original_filename size url uuid variations content_info metadata tags appdata source highlight
|
|
23
23
|
].freeze
|
|
24
24
|
|
|
25
25
|
attr_writer :uuid
|
|
26
26
|
attr_accessor :datetime_removed, :datetime_stored, :datetime_uploaded, :is_image, :is_ready, :mime_type,
|
|
27
27
|
:original_file_url, :original_filename, :size, :url, :variations, :content_info,
|
|
28
|
-
:metadata, :appdata, :source
|
|
28
|
+
:metadata, :tags, :appdata, :source, :highlight
|
|
29
29
|
|
|
30
30
|
# --- Class methods ---
|
|
31
31
|
|
|
@@ -45,11 +45,6 @@ class Uploadcare::Resources::File < Uploadcare::Resources::BaseResource
|
|
|
45
45
|
new(response, resolved_client)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
class << self
|
|
49
|
-
alias retrieve find
|
|
50
|
-
alias info find
|
|
51
|
-
end
|
|
52
|
-
|
|
53
48
|
# List files with optional filtering and pagination.
|
|
54
49
|
#
|
|
55
50
|
# @param options [Hash] Query parameters (limit, ordering, etc.)
|
|
@@ -78,6 +73,23 @@ class Uploadcare::Resources::File < Uploadcare::Resources::BaseResource
|
|
|
78
73
|
)
|
|
79
74
|
end
|
|
80
75
|
|
|
76
|
+
# Search files with full-text criteria and structured filters.
|
|
77
|
+
#
|
|
78
|
+
# `limit`, `offset`, and `include` are sent as query parameters. All other
|
|
79
|
+
# options are sent in the JSON request body as search criteria.
|
|
80
|
+
#
|
|
81
|
+
# @param options [Hash] Search criteria plus pagination/expansion options
|
|
82
|
+
# @param client [Uploadcare::Client, nil] Client instance
|
|
83
|
+
# @param config [Uploadcare::Configuration] Configuration fallback
|
|
84
|
+
# @param request_options [Hash] Request options
|
|
85
|
+
# @return [Uploadcare::Collections::FileSearchResult]
|
|
86
|
+
def self.search(options: {}, client: nil, config: Uploadcare.configuration, request_options: {})
|
|
87
|
+
resolved_client = resolve_client(client: client, config: config)
|
|
88
|
+
Uploadcare::Operations::FileSearch.call(
|
|
89
|
+
options: options, client: resolved_client, resource_class: self, request_options: request_options
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
81
93
|
# Upload a single file.
|
|
82
94
|
#
|
|
83
95
|
# @param file [File, IO] File to upload
|
|
@@ -114,10 +126,6 @@ class Uploadcare::Resources::File < Uploadcare::Resources::BaseResource
|
|
|
114
126
|
resolved_client.uploads.upload_from_url(url: url, request_options: request_options, **options)
|
|
115
127
|
end
|
|
116
128
|
|
|
117
|
-
class << self
|
|
118
|
-
alias upload_from_url upload_url
|
|
119
|
-
end
|
|
120
|
-
|
|
121
129
|
# Batch store files.
|
|
122
130
|
#
|
|
123
131
|
# @param uuids [Array<String>] File UUIDs to store
|
|
@@ -175,10 +183,6 @@ class Uploadcare::Resources::File < Uploadcare::Resources::BaseResource
|
|
|
175
183
|
new(response['result'], resolved_client)
|
|
176
184
|
end
|
|
177
185
|
|
|
178
|
-
class << self
|
|
179
|
-
alias copy_to_local local_copy
|
|
180
|
-
end
|
|
181
|
-
|
|
182
186
|
# Copy a file to remote storage (class method).
|
|
183
187
|
#
|
|
184
188
|
# @param source [String] CDN URL or UUID
|
|
@@ -197,6 +201,10 @@ class Uploadcare::Resources::File < Uploadcare::Resources::BaseResource
|
|
|
197
201
|
end
|
|
198
202
|
|
|
199
203
|
class << self
|
|
204
|
+
alias retrieve find
|
|
205
|
+
alias info find
|
|
206
|
+
alias upload_from_url upload_url
|
|
207
|
+
alias copy_to_local local_copy
|
|
200
208
|
alias copy_to_remote remote_copy
|
|
201
209
|
end
|
|
202
210
|
|