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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/gem-push.yml +3 -1
  3. data/CHANGELOG.md +18 -0
  4. data/MIGRATING_V5.md +2 -1
  5. data/README.md +108 -6
  6. data/api_examples/README.md +9 -5
  7. data/api_examples/rest_api/get_files_uuid_tags.rb +4 -0
  8. data/api_examples/rest_api/patch_files_uuid_tags.rb +4 -0
  9. data/api_examples/rest_api/post_files_search.rb +4 -0
  10. data/api_examples/rest_api/put_files_uuid_tags.rb +4 -0
  11. data/api_examples/support/example_helper.rb +17 -1
  12. data/api_examples/support/run_rest_example.rb +30 -0
  13. data/api_examples/support/run_upload_example.rb +8 -2
  14. data/context7.json +3 -1
  15. data/docs/release-notes-5.1.0.md +33 -0
  16. data/examples/README.md +4 -0
  17. data/examples/file_search.rb +33 -0
  18. data/examples/file_tags.rb +37 -0
  19. data/lib/uploadcare/api/rest/file_tags.rb +62 -0
  20. data/lib/uploadcare/api/rest/files.rb +18 -1
  21. data/lib/uploadcare/api/rest.rb +37 -12
  22. data/lib/uploadcare/api/upload/files.rb +18 -5
  23. data/lib/uploadcare/api/upload.rb +2 -1
  24. data/lib/uploadcare/client/file_tags_accessor.rb +44 -0
  25. data/lib/uploadcare/client/files_accessor.rb +17 -0
  26. data/lib/uploadcare/client.rb +7 -0
  27. data/lib/uploadcare/collections/file_search_result.rb +45 -0
  28. data/lib/uploadcare/collections/paginated.rb +7 -1
  29. data/lib/uploadcare/internal/file_tag_normalizer.rb +62 -0
  30. data/lib/uploadcare/internal/upload_params_generator.rb +16 -2
  31. data/lib/uploadcare/internal/user_agent.rb +1 -1
  32. data/lib/uploadcare/operations/file_search.rb +43 -0
  33. data/lib/uploadcare/operations/multipart_upload.rb +1 -1
  34. data/lib/uploadcare/operations/upload_router.rb +2 -2
  35. data/lib/uploadcare/resources/file.rb +23 -15
  36. data/lib/uploadcare/resources/file_tags.rb +92 -0
  37. data/lib/uploadcare/version.rb +1 -1
  38. data/lib/uploadcare.rb +2 -0
  39. metadata +14 -1
@@ -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(method: :post, path: path, params: params, headers: headers, request_options: request_options)
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(method: method, path: path, params: params, headers: headers, request_options: request_options)
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
- uri = build_request_uri(path, params, upcase_method_name)
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, method)
181
- if method == HTTP_GET && !params.nil? && params.is_a?(Hash) && !params.empty?
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'] ||= Uploadcare::Internal::UserAgent.call(config: config)
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]
@@ -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.0.0/demopublickey (Ruby/3.3.0)"
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