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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dfba74b1784392911f6d3ced082ef949c314128aeba804958a54344b4a62b094
4
- data.tar.gz: e193a1d8260e99b44b7b903fae7497644decb72efeef206350bd76062e226579
3
+ metadata.gz: b0a3dc13f1f548e3f3083128b65c9df35524321b9ab73bff80b8eae0e6d52e0e
4
+ data.tar.gz: fe8b13b6f891f938aabe9d1fe39875981662c4f3f8e66b4eb234b9daf884a734
5
5
  SHA512:
6
- metadata.gz: bbfa444e6a5716c7539baf69c35711b2f16a79628376779289d6428732057a0906981476664c2f0e1a7a6a535ae8a9ceed9cf8ee5507e39b70a7a235a54e587e
7
- data.tar.gz: bb9708224d02016fa9b16df4104396a784f89bad12cdb5d84d601b4ffaf524e68f9ee2bb21899d16c0d8c93c78ff5463a277c40cbfd84efde9843ba7ddc93657
6
+ metadata.gz: 54cbc1ab6d88ee5f08c8cbb022d69417ffc2b064897f4db815582b3f997e143c943b178d44b3afc340b5307cb747cd7d5b7ba3a08e01b7026d99649a575cb62d
7
+ data.tar.gz: da4e93044e29484359810c3bcda60e720067f2add3de07f0666c7e94bda4d95c243d96c6c832b0f53f2aad62d2b902b8d2169d1e4c0e4a929514dc7eaf74152d
@@ -7,13 +7,15 @@ on:
7
7
  jobs:
8
8
  build:
9
9
  runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: write
10
12
 
11
13
  steps:
12
14
  - uses: actions/checkout@v5
13
15
 
14
16
  - name: Release Gem
15
17
  if: contains(github.ref, 'refs/tags/v')
16
- uses: cadwallion/publish-rubygems-action@master
18
+ uses: cadwallion/publish-rubygems-action@94a6f4cd5350581749c569b5001eecc864e3ad0b # v1.1.0
17
19
  env:
18
20
  GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
19
21
  RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.0 — 2026-09-06
4
+
5
+ ### Added
6
+
7
+ * Search files through `client.files.search` with full-text, UUID, exact, metadata, tag, range, and image filters,
8
+ sorting, highlights, `include=appdata`, and POST-aware pagination that preserves the original search options.
9
+ * Manage per-file tags through `client.file_tags` with list, replace, and atomic add/delete operations.
10
+ * Apply tags while uploading files directly, in batches, from URLs, or through multipart uploads.
11
+ * Read tags from file resources returned by the REST API.
12
+ * Call the raw REST file-search and file-tag endpoints through `client.api.rest` when exact endpoint parity is needed.
13
+
14
+ ## 5.0.1 — 2026-06-09
15
+
16
+ ### Fixed
17
+
18
+ * Upload API requests now send the SDK-generated `User-Agent` instead of Faraday's default header.
19
+ * Multipart upload part requests to presigned URLs now include the same SDK-generated `User-Agent`.
20
+
3
21
  ## 5.0.0 — 2026-05-17
4
22
 
5
23
  v5 is stable.
data/MIGRATING_V5.md CHANGED
@@ -27,7 +27,7 @@ The examples expect `UPLOADCARE_PUBLIC_KEY` and `UPLOADCARE_SECRET_KEY` to come
27
27
  ## Recommended Migration Order
28
28
 
29
29
  1. Introduce explicit `Uploadcare::Client` instances in your application.
30
- 2. Move app-facing code to `client.files`, `client.groups`, `client.uploads`, `client.project`, `client.webhooks`, `client.file_metadata`, `client.addons`, and `client.conversions`.
30
+ 2. Move app-facing code to `client.files`, `client.groups`, `client.uploads`, `client.project`, `client.webhooks`, `client.file_metadata`, `client.file_tags`, `client.addons`, and `client.conversions`.
31
31
  3. Keep `client.api.rest` and `client.api.upload` only where you need raw endpoint parity.
32
32
  4. Audit return-type and error-handling assumptions.
33
33
  5. Remove any app code that depends on internal transport classes.
@@ -201,6 +201,7 @@ These top-level resource constants still exist:
201
201
  - `Uploadcare::Project`
202
202
  - `Uploadcare::Webhook`
203
203
  - `Uploadcare::FileMetadata`
204
+ - `Uploadcare::FileTags`
204
205
  - `Uploadcare::DocumentConversion`
205
206
  - `Uploadcare::VideoConversion`
206
207
 
data/README.md CHANGED
@@ -27,6 +27,7 @@ The gem is built around:
27
27
  - [Multi-Account Usage](#multi-account-usage)
28
28
  - [Uploads](#uploads)
29
29
  - [Files](#files)
30
+ - [File Tags](#file-tags)
30
31
  - [Groups](#groups)
31
32
  - [Project](#project)
32
33
  - [Metadata](#metadata)
@@ -87,6 +88,7 @@ This is the default API you should use in applications:
87
88
  - `client.project`
88
89
  - `client.webhooks`
89
90
  - `client.file_metadata`
91
+ - `client.file_tags`
90
92
  - `client.addons`
91
93
  - `client.conversions`
92
94
 
@@ -262,7 +264,12 @@ remote_file = client.uploads.upload("https://example.com/image.jpg", store: true
262
264
 
263
265
  ```ruby
264
266
  file = File.open("photo.jpg", "rb") do |io|
265
- client.files.upload(io, store: true, metadata: { subsystem: "avatars" })
267
+ client.files.upload(
268
+ io,
269
+ store: true,
270
+ metadata: { subsystem: "avatars" },
271
+ tags: ["avatar", "profile"]
272
+ )
266
273
  end
267
274
  ```
268
275
 
@@ -274,7 +281,7 @@ files = [
274
281
  File.open("photo-2.jpg", "rb")
275
282
  ]
276
283
 
277
- uploaded = client.uploads.upload(files, store: true)
284
+ uploaded = client.uploads.upload(files, store: true, tags: ["gallery", "batch"])
278
285
 
279
286
  files.each(&:close)
280
287
  ```
@@ -284,7 +291,11 @@ files.each(&:close)
284
291
  Synchronous:
285
292
 
286
293
  ```ruby
287
- file = client.files.upload_from_url("https://example.com/image.jpg", store: true)
294
+ file = client.files.upload_from_url(
295
+ "https://example.com/image.jpg",
296
+ store: true,
297
+ tags: ["remote", "example"]
298
+ )
288
299
  ```
289
300
 
290
301
  Async:
@@ -306,7 +317,12 @@ Polling options for synchronous URL uploads:
306
317
 
307
318
  ```ruby
308
319
  File.open("large-video.mp4", "rb") do |io|
309
- file = client.uploads.multipart_upload(file: io, store: true, threads: 4) do |progress|
320
+ file = client.uploads.multipart_upload(
321
+ file: io,
322
+ store: true,
323
+ threads: 4,
324
+ tags: ["video", "multipart"]
325
+ ) do |progress|
310
326
  uploaded = progress[:uploaded]
311
327
  total = progress[:total]
312
328
  puts "#{uploaded}/#{total}"
@@ -342,6 +358,7 @@ Common upload options:
342
358
 
343
359
  - `store: true | false | "auto"`
344
360
  - `metadata: { key: value }`
361
+ - `tags: ["tag-1", "tag_2"]`
345
362
  - `signature: "..."`
346
363
  - `expire: unix_timestamp`
347
364
  - `async: true` for URL uploads
@@ -389,6 +406,50 @@ Filters and API parameters can still be passed through:
389
406
  files = client.files.list(stored: true, removed: false, limit: 100)
390
407
  ```
391
408
 
409
+ ### Search files
410
+
411
+ Search across filenames, file UUIDs, metadata, and detected MIME types:
412
+
413
+ ```ruby
414
+ matches = client.files.search(
415
+ query: "invoice",
416
+ is_image: false,
417
+ sort: ["-datetime_uploaded"],
418
+ limit: 20
419
+ )
420
+
421
+ puts "Found #{matches.total} files"
422
+ matches.each do |file|
423
+ puts file.original_filename
424
+ puts file.highlight
425
+ end
426
+ ```
427
+
428
+ Search accepts full-text `query` and field-specific `phrase` criteria, exact matches, ranges, and tag filters. All
429
+ top-level criteria are combined with AND:
430
+
431
+ ```ruby
432
+ matches = client.files.search(
433
+ phrase: { original_filename: "report" },
434
+ exact: {
435
+ detected_mime_type: ["application/pdf"],
436
+ "metadata[department]" => ["finance"]
437
+ },
438
+ datetime_uploaded: { gte: "2026-01-01T00:00:00Z" },
439
+ size: { lte: 10 * 1024 * 1024 },
440
+ tags: { all: ["approved"], none: ["archived"] },
441
+ include: "appdata"
442
+ )
443
+ ```
444
+
445
+ `limit`, `offset`, and `include` are sent as URL query parameters; search criteria are sent in the JSON body. Search
446
+ responses are `Uploadcare::Collections::FileSearchResult` objects and support `next_page`, `previous_page`, and `all`
447
+ like ordinary file lists. Subsequent pages automatically resend the original search criteria. Full-text values must be
448
+ at least four characters; use `exact` for shorter values. A field cannot appear in both `phrase` and `exact`, and every
449
+ request needs at least one search condition. `limit` accepts 1–100 results, while `offset + limit` cannot exceed 1,000.
450
+ For filter-only searches, pass an explicit `sort` for deterministic ordering. `fuzziness: true` enables typo-tolerant
451
+ matching but increases latency. Newly uploaded files may take a short time to appear in the search index.
452
+
392
453
  ### Resource operations
393
454
 
394
455
  ```ruby
@@ -424,6 +485,43 @@ copied = file.copy_to_local(options: { store: true })
424
485
  remote_url = file.copy_to_remote(target: "custom_storage")
425
486
  ```
426
487
 
488
+ File responses expose the ordered tag list through `file.tags` when the field is present.
489
+
490
+ ## File Tags
491
+
492
+ Tags can be attached during direct, URL, batch, and multipart uploads with the `tags:` option. The SDK normalizes tags to lowercase, strips surrounding whitespace, discards blank tags, removes duplicates while preserving order, and validates the platform limits.
493
+ Upload responses do not include tags; use `client.file_tags.list` or reload the file through the REST API to read them.
494
+
495
+ Read or replace the complete tag list:
496
+
497
+ ```ruby
498
+ tags = client.file_tags.list(uuid: file.uuid)
499
+
500
+ change = client.file_tags.replace(
501
+ uuid: file.uuid,
502
+ tags: ["approved", "Summer"]
503
+ )
504
+
505
+ puts change.tags
506
+ puts change.added
507
+ puts change.deleted
508
+ ```
509
+
510
+ Mutation responses expose the resulting ordered `tags`, the tags actually `added`, and the tags actually `deleted`.
511
+ Replacing with the same normalized set is safe and reports no additions or deletions.
512
+
513
+ Add and delete tags atomically (deletions are applied first):
514
+
515
+ ```ruby
516
+ change = client.file_tags.update(
517
+ uuid: file.uuid,
518
+ add: ["featured"],
519
+ delete: ["draft"]
520
+ )
521
+ ```
522
+
523
+ Passing an empty array to `replace` clears all tags. Tags may contain Latin letters, digits, hyphens, underscores, and dots; each tag is limited to 100 characters and each file to 50 tags.
524
+
427
525
  ## Groups
428
526
 
429
527
  Create a group:
@@ -608,10 +706,14 @@ Upload API:
608
706
 
609
707
  ```ruby
610
708
  File.open("photo.jpg", "rb") do |io|
611
- client.api.upload.files.direct(file: io, store: true)
709
+ client.api.upload.files.direct(file: io, store: true, tags: ["photo", "example"])
612
710
  end
613
711
 
614
- client.api.upload.files.from_url(source_url: "https://example.com/image.jpg", async: true)
712
+ client.api.upload.files.from_url(
713
+ source_url: "https://example.com/image.jpg",
714
+ async: true,
715
+ tags: ["remote", "example"]
716
+ )
615
717
  client.api.upload.groups.create(files: ["uuid-1", "uuid-2"])
616
718
  ```
617
719
 
@@ -21,14 +21,15 @@ Optional environment variables:
21
21
 
22
22
  Verification:
23
23
 
24
- - Verified against a real Uploadcare demo account on `2026-03-16`
25
- - All canonical scripts in `api_examples/rest_api` and `api_examples/upload_api` executed successfully
24
+ - The endpoint suite was verified against a real Uploadcare demo account on `2026-08-07`.
25
+ - Run changed examples against a disposable project before release; they create and remove temporary resources.
26
26
 
27
27
  ## REST API 0.7
28
28
 
29
29
  | Endpoint | Example file | Notes |
30
30
  | --- | --- | --- |
31
31
  | `GET /files/` | `api_examples/rest_api/get_files.rb` | Uses `client.files.list` |
32
+ | `POST /files/search/` | `api_examples/rest_api/post_files_search.rb` | Uses `client.files.search` with a UUID lookup |
32
33
  | `PUT /files/{uuid}/storage/` | `api_examples/rest_api/put_files_uuid_storage.rb` | Uses `file.store` |
33
34
  | `DELETE /files/{uuid}/storage/` | `api_examples/rest_api/delete_files_uuid_storage.rb` | Uses `file.delete` |
34
35
  | `GET /files/{uuid}/` | `api_examples/rest_api/get_files_uuid.rb` | Uses `client.files.find` |
@@ -40,6 +41,9 @@ Verification:
40
41
  | `GET /files/{uuid}/metadata/{key}/` | `api_examples/rest_api/get_files_uuid_metadata_key.rb` | Uses `client.file_metadata.show` |
41
42
  | `PUT /files/{uuid}/metadata/{key}/` | `api_examples/rest_api/put_files_uuid_metadata_key.rb` | Uses `client.file_metadata.update` |
42
43
  | `DELETE /files/{uuid}/metadata/{key}/` | `api_examples/rest_api/delete_files_uuid_metadata_key.rb` | Uses `client.file_metadata.delete` |
44
+ | `GET /files/{uuid}/tags/` | `api_examples/rest_api/get_files_uuid_tags.rb` | Uses `client.file_tags.list` |
45
+ | `PUT /files/{uuid}/tags/` | `api_examples/rest_api/put_files_uuid_tags.rb` | Uses `client.file_tags.replace` |
46
+ | `PATCH /files/{uuid}/tags/` | `api_examples/rest_api/patch_files_uuid_tags.rb` | Uses `client.file_tags.update` |
43
47
  | `GET /groups/` | `api_examples/rest_api/get_groups.rb` | Uses `client.groups.list` |
44
48
  | `GET /groups/{uuid}/` | `api_examples/rest_api/get_groups_uuid.rb` | Uses `client.groups.find` |
45
49
  | `DELETE /groups/{uuid}/` | `api_examples/rest_api/delete_groups_uuid.rb` | Uses `group.delete` |
@@ -66,11 +70,11 @@ Verification:
66
70
 
67
71
  | Endpoint | Example file | Notes |
68
72
  | --- | --- | --- |
69
- | `POST /base/` | `api_examples/upload_api/post_base.rb` | Uses raw upload API |
70
- | `POST /multipart/start/` | `api_examples/upload_api/post_multipart_start.rb` | Starts and completes a real multipart upload |
73
+ | `POST /base/` | `api_examples/upload_api/post_base.rb` | Uses raw upload API with upload-time tags |
74
+ | `POST /multipart/start/` | `api_examples/upload_api/post_multipart_start.rb` | Starts and completes a real multipart upload with tags |
71
75
  | `PUT <presigned-url-x>` | `api_examples/upload_api/put_multipart_part.rb` | Uploads one part via gem multipart helper |
72
76
  | `POST /multipart/complete/` | `api_examples/upload_api/post_multipart_complete.rb` | Completes a real multipart upload |
73
- | `POST /from_url/` | `api_examples/upload_api/post_from_url.rb` | Uses raw upload API |
77
+ | `POST /from_url/` | `api_examples/upload_api/post_from_url.rb` | Uses raw upload API with upload-time tags |
74
78
  | `GET /from_url/status/` | `api_examples/upload_api/get_from_url_status.rb` | Starts async upload then checks status |
75
79
  | `GET /info/` | `api_examples/upload_api/get_info.rb` | Uses raw upload API |
76
80
  | `POST /group/` | `api_examples/upload_api/post_group.rb` | Uses raw upload API |
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../support/run_rest_example'
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../support/run_rest_example'
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../support/run_rest_example'
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../support/run_rest_example'
@@ -66,6 +66,21 @@ module ApiExamples::ExampleHelper
66
66
  Array(files).each { |file| safe_delete_file(file) }
67
67
  end
68
68
 
69
+ def wait_for_file_search(uuid:, timeout: 10, poll_interval: 0.5)
70
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
71
+
72
+ loop do
73
+ matches = client.files.search(query: uuid, limit: 20)
74
+ return matches if matches.any? { |match| match.uuid == uuid }
75
+
76
+ raise "Timed out waiting for file #{uuid} to appear in search" if Process.clock_gettime(
77
+ Process::CLOCK_MONOTONIC
78
+ ) >= deadline
79
+
80
+ sleep poll_interval
81
+ end
82
+ end
83
+
69
84
  def with_fixture_file(name)
70
85
  handle = File.open(fixture_path(name), 'rb')
71
86
  response = yield handle
@@ -124,7 +139,8 @@ module ApiExamples::ExampleHelper
124
139
  size: file.size,
125
140
  content_type: 'image/jpeg',
126
141
  part_size: multipart_part_size,
127
- store: true
142
+ store: true,
143
+ tags: %w[example multipart]
128
144
  )
129
145
  )
130
146
  yield file, response
@@ -12,6 +12,20 @@ module ApiExamples::RunRestExample
12
12
  client.project.current
13
13
  when 'get_files.rb'
14
14
  client.files.list(limit: 2)
15
+ when 'post_files_search.rb'
16
+ ApiExamples::ExampleHelper.with_uploaded_file do |file|
17
+ matches = ApiExamples::ExampleHelper.wait_for_file_search(uuid: file.uuid)
18
+ {
19
+ 'total' => matches.total,
20
+ 'results' => matches.map do |match|
21
+ {
22
+ 'uuid' => match.uuid,
23
+ 'original_filename' => match.original_filename,
24
+ 'highlight' => match.highlight
25
+ }
26
+ end
27
+ }
28
+ end
15
29
  when 'get_files_uuid.rb'
16
30
  ApiExamples::ExampleHelper.with_uploaded_file do |file|
17
31
  client.files.find(uuid: file.uuid)
@@ -93,6 +107,22 @@ module ApiExamples::RunRestExample
93
107
  client.file_metadata.delete(uuid: file.uuid, key: 'color')
94
108
  { 'uuid' => file.uuid, 'key' => 'color', 'deleted' => true }
95
109
  end
110
+ when 'get_files_uuid_tags.rb'
111
+ ApiExamples::ExampleHelper.with_uploaded_file do |file|
112
+ client.file_tags.replace(uuid: file.uuid, tags: %w[cat example])
113
+ client.file_tags.list(uuid: file.uuid)
114
+ end
115
+ when 'put_files_uuid_tags.rb'
116
+ ApiExamples::ExampleHelper.with_uploaded_file do |file|
117
+ change = client.file_tags.replace(uuid: file.uuid, tags: %w[approved example])
118
+ { 'tags' => change.tags, 'added' => change.added, 'deleted' => change.deleted }
119
+ end
120
+ when 'patch_files_uuid_tags.rb'
121
+ ApiExamples::ExampleHelper.with_uploaded_file do |file|
122
+ client.file_tags.replace(uuid: file.uuid, tags: %w[draft example])
123
+ change = client.file_tags.update(uuid: file.uuid, add: ['featured'], delete: ['draft'])
124
+ { 'tags' => change.tags, 'added' => change.added, 'deleted' => change.deleted }
125
+ end
96
126
  when 'post_addons_aws_rekognition_detect_labels_execute.rb'
97
127
  ApiExamples::ExampleHelper.with_uploaded_file do |file|
98
128
  client.addons.aws_rekognition_detect_labels(uuid: file.uuid)
@@ -71,13 +71,19 @@ module ApiExamples::RunUploadExample
71
71
 
72
72
  def run_base_upload(client)
73
73
  ApiExamples::ExampleHelper.with_fixture_file('kitten.jpeg') do |handle|
74
- ApiExamples::ExampleHelper.unwrap(client.api.upload.files.direct(file: handle, store: true))
74
+ ApiExamples::ExampleHelper.unwrap(
75
+ client.api.upload.files.direct(file: handle, store: true, tags: %w[example base])
76
+ )
75
77
  end
76
78
  end
77
79
 
78
80
  def run_url_upload(client)
79
81
  response = ApiExamples::ExampleHelper.unwrap(
80
- client.api.upload.files.from_url(source_url: ApiExamples::ExampleHelper::SAMPLE_IMAGE_URL, store: true)
82
+ client.api.upload.files.from_url(
83
+ source_url: ApiExamples::ExampleHelper::SAMPLE_IMAGE_URL,
84
+ store: true,
85
+ tags: %w[example remote]
86
+ )
81
87
  )
82
88
  response
83
89
  ensure
data/context7.json CHANGED
@@ -52,9 +52,11 @@
52
52
  "Prefer explicit `Uploadcare::Client` instances for application code, especially when an app uses more than one Uploadcare project.",
53
53
  "Configure credentials with `Uploadcare::Client.new(public_key: ..., secret_key: ...)`, `Uploadcare.configure`, or the UPLOADCARE_PUBLIC_KEY and UPLOADCARE_SECRET_KEY environment variables.",
54
54
  "Never hardcode real Uploadcare API keys in examples or application code; use environment variables or an application secrets store.",
55
- "Use `client.files`, `client.groups`, `client.uploads`, `client.project`, `client.webhooks`, `client.file_metadata`, `client.addons`, and `client.conversions` for normal application workflows.",
55
+ "Use `client.files`, `client.groups`, `client.uploads`, `client.project`, `client.webhooks`, `client.file_metadata`, `client.file_tags`, `client.addons`, and `client.conversions` for normal application workflows.",
56
56
  "Use `client.api.rest` and `client.api.upload` when exact REST API or Upload API endpoint parity is needed.",
57
57
  "Use `client.uploads.upload` for automatic upload method selection and `client.uploads.multipart_upload` for explicit multipart uploads.",
58
+ "Use `client.files.search` for full-text and structured file search; search pagination resends the original POST criteria automatically.",
59
+ "Pass `tags: [...]` when uploading, and use `client.file_tags` to list, replace, add, or delete tags on an existing file.",
58
60
  "Use `MIGRATING_V5.md` when upgrading applications from uploadcare-ruby v4.x to v5."
59
61
  ],
60
62
  "previousVersions": [
@@ -0,0 +1,33 @@
1
+ # uploadcare-ruby 5.1.0
2
+
3
+ This release adds file search and file-tag support to the v5 client API. It is backward compatible with 5.0.x and
4
+ does not require an application migration.
5
+
6
+ ## Highlights
7
+
8
+ - Search files through `client.files.search` using text, UUID, metadata, tag, range, and image filters.
9
+ - Sort search results, inspect match highlights, request `appdata`, and paginate without rebuilding POST criteria.
10
+ - List, replace, add, and delete per-file tags through `client.file_tags`.
11
+ - Assign tags during direct, batch, URL, and multipart uploads.
12
+ - Read tags directly from returned file resources.
13
+ - Use `client.api.rest.files.search` and `client.api.rest.file_tags` for exact REST endpoint access.
14
+
15
+ ## Upgrade Notes
16
+
17
+ - Requirement: Ruby `>= 3.3`.
18
+ - Update with `bundle update uploadcare-ruby` and run the application's test suite.
19
+ - No configuration or data migration is required from 5.0.x.
20
+ - File search results may be eventually consistent immediately after an upload; retry when a workflow searches for a
21
+ file it just created.
22
+ - Existing upload calls remain valid. Pass `tags: [...]` only where upload-time tagging is needed.
23
+ - Rollback: pin `uploadcare-ruby` to `5.0.1`.
24
+
25
+ ## Examples
26
+
27
+ - File search: [`examples/file_search.rb`](../examples/file_search.rb)
28
+ - File tags: [`examples/file_tags.rb`](../examples/file_tags.rb)
29
+ - Raw REST examples: [`api_examples/README.md`](../api_examples/README.md)
30
+
31
+ ## Full Changelog
32
+
33
+ See [CHANGELOG.md](../CHANGELOG.md).
data/examples/README.md CHANGED
@@ -29,6 +29,10 @@ mise exec -- ruby examples/simple_upload.rb spec/fixtures/kitten.jpeg
29
29
  Force multipart upload and show throughput details.
30
30
  - `examples/url_upload.rb`
31
31
  Upload a remote URL and show async polling as a follow-up example.
32
+ - `examples/file_search.rb`
33
+ Search files by text, inspect highlights, and follow the next page.
34
+ - `examples/file_tags.rb`
35
+ Upload a file with tags, then list, replace, add, and delete tags.
32
36
  - `examples/group_creation.rb`
33
37
  Upload multiple files, create a group, and print group details.
34
38
 
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/uploadcare'
5
+ require 'dotenv/load'
6
+
7
+ query = ARGV.join(' ')
8
+
9
+ if query.length < 4
10
+ puts 'Usage: ruby file_search.rb <query of at least four characters>'
11
+ puts 'Example: ruby file_search.rb invoice'
12
+ exit 1
13
+ end
14
+
15
+ begin
16
+ client = Uploadcare::Client.new(
17
+ public_key: ENV.fetch('UPLOADCARE_PUBLIC_KEY'),
18
+ secret_key: ENV.fetch('UPLOADCARE_SECRET_KEY')
19
+ )
20
+
21
+ results = client.files.search(query: query, sort: ['-datetime_uploaded'], limit: 20)
22
+
23
+ puts "Found #{results.total} files"
24
+ results.each do |file|
25
+ puts [file.uuid, file.original_filename, file.highlight].compact.join(' | ')
26
+ end
27
+
28
+ next_page = results.next_page
29
+ puts "Next page contains #{next_page.count} files" if next_page
30
+ rescue StandardError => e
31
+ warn "File search example failed: #{e.message}"
32
+ exit 1
33
+ end
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/uploadcare'
5
+ require 'dotenv/load'
6
+
7
+ file_path = ARGV[0]
8
+
9
+ unless file_path && File.exist?(file_path)
10
+ puts 'Usage: ruby file_tags.rb <file_path>'
11
+ puts 'Example: ruby file_tags.rb photo.jpg'
12
+ exit 1
13
+ end
14
+
15
+ begin
16
+ client = Uploadcare::Client.new(
17
+ public_key: ENV.fetch('UPLOADCARE_PUBLIC_KEY'),
18
+ secret_key: ENV.fetch('UPLOADCARE_SECRET_KEY')
19
+ )
20
+
21
+ file = File.open(file_path, 'rb') do |io|
22
+ client.files.upload(io, store: true, tags: %w[example draft])
23
+ end
24
+
25
+ puts "Uploaded #{file.uuid} with tags: #{client.file_tags.list(uuid: file.uuid).join(', ')}"
26
+
27
+ change = client.file_tags.replace(uuid: file.uuid, tags: %w[example approved])
28
+ puts "Replaced tags: #{change.tags.join(', ')}"
29
+
30
+ change = client.file_tags.update(uuid: file.uuid, add: ['featured'], delete: ['example'])
31
+ puts "Updated tags: #{change.tags.join(', ')}"
32
+ rescue StandardError => e
33
+ warn "File tag example failed: #{e.message}"
34
+ exit 1
35
+ ensure
36
+ client.api.rest.files.delete(uuid: file.uuid) if file&.uuid
37
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ # REST API endpoint for per-file tag operations.
6
+ #
7
+ # @see https://uploadcare.com/api-refs/rest-api/v0.7.0/#tag/File-tags
8
+ class Uploadcare::Api::Rest::FileTags
9
+ # @return [Uploadcare::Api::Rest] Parent REST client
10
+ attr_reader :rest
11
+
12
+ # @param rest [Uploadcare::Api::Rest] Parent REST client
13
+ def initialize(rest:)
14
+ @rest = rest
15
+ end
16
+
17
+ # Get the ordered list of tags for a file.
18
+ #
19
+ # @param uuid [String] File UUID
20
+ # @param request_options [Hash] Request options
21
+ # @return [Uploadcare::Result] Response containing the `tags` array
22
+ def list(uuid:, request_options: {})
23
+ rest.get(path: tags_path(uuid), params: {}, headers: {}, request_options: request_options)
24
+ end
25
+ alias index list
26
+
27
+ # Replace all tags for a file.
28
+ #
29
+ # @param uuid [String] File UUID
30
+ # @param tags [Array<String>] Complete replacement tag list
31
+ # @param request_options [Hash] Request options
32
+ # @return [Uploadcare::Result] Response containing tags, added, and deleted
33
+ def replace(uuid:, tags:, request_options: {})
34
+ rest.put(
35
+ path: tags_path(uuid), params: { tags: tags }, headers: {}, request_options: request_options
36
+ )
37
+ end
38
+
39
+ # Atomically add and delete tags for a file.
40
+ #
41
+ # Deletions are applied before additions by the API.
42
+ #
43
+ # @param uuid [String] File UUID
44
+ # @param add [Array<String>, nil] Tags to add
45
+ # @param delete [Array<String>, nil] Tags to delete
46
+ # @param request_options [Hash] Request options
47
+ # @return [Uploadcare::Result] Response containing tags, added, and deleted
48
+ def update(uuid:, add: nil, delete: nil, request_options: {})
49
+ params = {}
50
+ params[:add] = add unless add.nil? || add.empty?
51
+ params[:delete] = delete unless delete.nil? || delete.empty?
52
+ body = params.empty? ? {}.to_json : params
53
+ rest.patch(path: tags_path(uuid), params: body, headers: {}, request_options: request_options)
54
+ end
55
+
56
+ private
57
+
58
+ def tags_path(uuid)
59
+ encoded_uuid = URI.encode_www_form_component(uuid.to_s)
60
+ "/files/#{encoded_uuid}/tags/"
61
+ end
62
+ end
@@ -2,11 +2,12 @@
2
2
 
3
3
  # REST API endpoint for file operations.
4
4
  #
5
- # Provides methods for listing, retrieving, storing, deleting, and copying files.
5
+ # Provides methods for listing, searching, retrieving, storing, deleting, and copying files.
6
6
  #
7
7
  # @example
8
8
  # rest = Uploadcare::Api::Rest.new(config: config)
9
9
  # rest.files.list(params: { limit: 10 })
10
+ # rest.files.search(params: { query: "invoice" }, query: { limit: 20 })
10
11
  # rest.files.info(uuid: "file-uuid")
11
12
  #
12
13
  # @see https://uploadcare.com/api-refs/rest-api/v0.7.0/#tag/File
@@ -29,6 +30,22 @@ class Uploadcare::Api::Rest::Files
29
30
  rest.get(path: '/files/', params: params, headers: {}, request_options: request_options)
30
31
  end
31
32
 
33
+ # Search files by full-text criteria and structured filters.
34
+ #
35
+ # Search criteria belong in the JSON request body (`params`). Pagination and
36
+ # response expansion options belong in the query string (`query`).
37
+ #
38
+ # @param params [Hash] Search body (query, phrase, exact, ranges, tags, etc.)
39
+ # @param query [Hash] Query parameters (limit, offset, include)
40
+ # @param request_options [Hash] Request options
41
+ # @return [Uploadcare::Result] Paginated file search results
42
+ # @see https://uploadcare.com/api-refs/rest-api/v0.7.0/#tag/File/operation/searchFiles
43
+ def search(params: {}, query: {}, request_options: {})
44
+ rest.post(
45
+ path: '/files/search/', params: params, query: query, headers: {}, request_options: request_options
46
+ )
47
+ end
48
+
32
49
  # Get file information by UUID.
33
50
  #
34
51
  # @param uuid [String] The file UUID