Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Unreleased

### Added

* Per-file tags through `client.file_tags` with list, replace, and atomic add/delete operations
* Upload-time `tags:` support for direct, batch, URL, and multipart uploads
* The `tags` attribute on file resources returned by the REST API

## 5.0.0 — 2026-05-17

v5 is stable.
Expand Down
70 changes: 64 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The gem is built around:
- [Multi-Account Usage](#multi-account-usage)
- [Uploads](#uploads)
- [Files](#files)
- [File Tags](#file-tags)
- [Groups](#groups)
- [Project](#project)
- [Metadata](#metadata)
Expand Down Expand Up @@ -87,6 +88,7 @@ This is the default API you should use in applications:
- `client.project`
- `client.webhooks`
- `client.file_metadata`
- `client.file_tags`
- `client.addons`
- `client.conversions`

Expand Down Expand Up @@ -262,7 +264,12 @@ remote_file = client.uploads.upload("https://example.com/image.jpg", store: true

```ruby
file = File.open("photo.jpg", "rb") do |io|
client.files.upload(io, store: true, metadata: { subsystem: "avatars" })
client.files.upload(
io,
store: true,
metadata: { subsystem: "avatars" },
tags: ["avatar", "profile"]
)
end
```

Expand All @@ -274,7 +281,7 @@ files = [
File.open("photo-2.jpg", "rb")
]

uploaded = client.uploads.upload(files, store: true)
uploaded = client.uploads.upload(files, store: true, tags: ["gallery", "batch"])

files.each(&:close)
```
Expand All @@ -284,7 +291,11 @@ files.each(&:close)
Synchronous:

```ruby
file = client.files.upload_from_url("https://example.com/image.jpg", store: true)
file = client.files.upload_from_url(
"https://example.com/image.jpg",
store: true,
tags: ["remote", "example"]
)
```

Async:
Expand All @@ -306,7 +317,12 @@ Polling options for synchronous URL uploads:

```ruby
File.open("large-video.mp4", "rb") do |io|
file = client.uploads.multipart_upload(file: io, store: true, threads: 4) do |progress|
file = client.uploads.multipart_upload(
file: io,
store: true,
threads: 4,
tags: ["video", "multipart"]
) do |progress|
uploaded = progress[:uploaded]
total = progress[:total]
puts "#{uploaded}/#{total}"
Expand Down Expand Up @@ -342,6 +358,7 @@ Common upload options:

- `store: true | false | "auto"`
- `metadata: { key: value }`
- `tags: ["tag-1", "tag_2"]`
- `signature: "..."`
- `expire: unix_timestamp`
- `async: true` for URL uploads
Expand Down Expand Up @@ -424,6 +441,43 @@ copied = file.copy_to_local(options: { store: true })
remote_url = file.copy_to_remote(target: "custom_storage")
```

File responses expose the ordered tag list through `file.tags` when the field is present.

## File Tags

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.
Upload responses do not include tags; use `client.file_tags.list` or reload the file through the REST API to read them.

Read or replace the complete tag list:

```ruby
tags = client.file_tags.list(uuid: file.uuid)

change = client.file_tags.replace(
uuid: file.uuid,
tags: ["approved", "Summer"]
)

puts change.tags
puts change.added
puts change.deleted
```

Mutation responses expose the resulting ordered `tags`, the tags actually `added`, and the tags actually `deleted`.
Replacing with the same normalized set is safe and reports no additions or deletions.

Add and delete tags atomically (deletions are applied first):

```ruby
change = client.file_tags.update(
uuid: file.uuid,
add: ["featured"],
delete: ["draft"]
)
```

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.

## Groups

Create a group:
Expand Down Expand Up @@ -608,10 +662,14 @@ Upload API:

```ruby
File.open("photo.jpg", "rb") do |io|
client.api.upload.files.direct(file: io, store: true)
client.api.upload.files.direct(file: io, store: true, tags: ["photo", "example"])
end

client.api.upload.files.from_url(source_url: "https://example.com/image.jpg", async: true)
client.api.upload.files.from_url(
source_url: "https://example.com/image.jpg",
async: true,
tags: ["remote", "example"]
)
client.api.upload.groups.create(files: ["uuid-1", "uuid-2"])
```

Expand Down
13 changes: 8 additions & 5 deletions api_examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ Optional environment variables:

Verification:

- Verified against a real Uploadcare demo account on `2026-03-16`
- All canonical scripts in `api_examples/rest_api` and `api_examples/upload_api` executed successfully
- The endpoint suite was verified against a real Uploadcare demo account on `2026-08-07`.
- Run changed examples against a disposable project before release; they create and remove temporary resources.

## REST API 0.7

Expand All @@ -40,6 +40,9 @@ Verification:
| `GET /files/{uuid}/metadata/{key}/` | `api_examples/rest_api/get_files_uuid_metadata_key.rb` | Uses `client.file_metadata.show` |
| `PUT /files/{uuid}/metadata/{key}/` | `api_examples/rest_api/put_files_uuid_metadata_key.rb` | Uses `client.file_metadata.update` |
| `DELETE /files/{uuid}/metadata/{key}/` | `api_examples/rest_api/delete_files_uuid_metadata_key.rb` | Uses `client.file_metadata.delete` |
| `GET /files/{uuid}/tags/` | `api_examples/rest_api/get_files_uuid_tags.rb` | Uses `client.file_tags.list` |
| `PUT /files/{uuid}/tags/` | `api_examples/rest_api/put_files_uuid_tags.rb` | Uses `client.file_tags.replace` |
| `PATCH /files/{uuid}/tags/` | `api_examples/rest_api/patch_files_uuid_tags.rb` | Uses `client.file_tags.update` |
| `GET /groups/` | `api_examples/rest_api/get_groups.rb` | Uses `client.groups.list` |
| `GET /groups/{uuid}/` | `api_examples/rest_api/get_groups_uuid.rb` | Uses `client.groups.find` |
| `DELETE /groups/{uuid}/` | `api_examples/rest_api/delete_groups_uuid.rb` | Uses `group.delete` |
Expand All @@ -66,11 +69,11 @@ Verification:

| Endpoint | Example file | Notes |
| --- | --- | --- |
| `POST /base/` | `api_examples/upload_api/post_base.rb` | Uses raw upload API |
| `POST /multipart/start/` | `api_examples/upload_api/post_multipart_start.rb` | Starts and completes a real multipart upload |
| `POST /base/` | `api_examples/upload_api/post_base.rb` | Uses raw upload API with upload-time tags |
| `POST /multipart/start/` | `api_examples/upload_api/post_multipart_start.rb` | Starts and completes a real multipart upload with tags |
| `PUT <presigned-url-x>` | `api_examples/upload_api/put_multipart_part.rb` | Uploads one part via gem multipart helper |
| `POST /multipart/complete/` | `api_examples/upload_api/post_multipart_complete.rb` | Completes a real multipart upload |
| `POST /from_url/` | `api_examples/upload_api/post_from_url.rb` | Uses raw upload API |
| `POST /from_url/` | `api_examples/upload_api/post_from_url.rb` | Uses raw upload API with upload-time tags |
| `GET /from_url/status/` | `api_examples/upload_api/get_from_url_status.rb` | Starts async upload then checks status |
| `GET /info/` | `api_examples/upload_api/get_info.rb` | Uses raw upload API |
| `POST /group/` | `api_examples/upload_api/post_group.rb` | Uses raw upload API |
Expand Down
4 changes: 4 additions & 0 deletions api_examples/rest_api/get_files_uuid_tags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative '../support/run_rest_example'
4 changes: 4 additions & 0 deletions api_examples/rest_api/patch_files_uuid_tags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative '../support/run_rest_example'
4 changes: 4 additions & 0 deletions api_examples/rest_api/put_files_uuid_tags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative '../support/run_rest_example'
3 changes: 2 additions & 1 deletion api_examples/support/example_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ def with_multipart_session
size: file.size,
content_type: 'image/jpeg',
part_size: multipart_part_size,
store: true
store: true,
tags: %w[example multipart]
)
)
yield file, response
Expand Down
16 changes: 16 additions & 0 deletions api_examples/support/run_rest_example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,22 @@ def call
client.file_metadata.delete(uuid: file.uuid, key: 'color')
{ 'uuid' => file.uuid, 'key' => 'color', 'deleted' => true }
end
when 'get_files_uuid_tags.rb'
ApiExamples::ExampleHelper.with_uploaded_file do |file|
client.file_tags.replace(uuid: file.uuid, tags: %w[cat example])
client.file_tags.list(uuid: file.uuid)
end
when 'put_files_uuid_tags.rb'
ApiExamples::ExampleHelper.with_uploaded_file do |file|
change = client.file_tags.replace(uuid: file.uuid, tags: %w[approved example])
{ 'tags' => change.tags, 'added' => change.added, 'deleted' => change.deleted }
end
when 'patch_files_uuid_tags.rb'
ApiExamples::ExampleHelper.with_uploaded_file do |file|
client.file_tags.replace(uuid: file.uuid, tags: %w[draft example])
change = client.file_tags.update(uuid: file.uuid, add: ['featured'], delete: ['draft'])
{ 'tags' => change.tags, 'added' => change.added, 'deleted' => change.deleted }
end
when 'post_addons_aws_rekognition_detect_labels_execute.rb'
ApiExamples::ExampleHelper.with_uploaded_file do |file|
client.addons.aws_rekognition_detect_labels(uuid: file.uuid)
Expand Down
10 changes: 8 additions & 2 deletions api_examples/support/run_upload_example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,19 @@ def call

def run_base_upload(client)
ApiExamples::ExampleHelper.with_fixture_file('kitten.jpeg') do |handle|
ApiExamples::ExampleHelper.unwrap(client.api.upload.files.direct(file: handle, store: true))
ApiExamples::ExampleHelper.unwrap(
client.api.upload.files.direct(file: handle, store: true, tags: %w[example base])
)
end
end

def run_url_upload(client)
response = ApiExamples::ExampleHelper.unwrap(
client.api.upload.files.from_url(source_url: ApiExamples::ExampleHelper::SAMPLE_IMAGE_URL, store: true)
client.api.upload.files.from_url(
source_url: ApiExamples::ExampleHelper::SAMPLE_IMAGE_URL,
store: true,
tags: %w[example remote]
)
)
response
ensure
Expand Down
3 changes: 2 additions & 1 deletion context7.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@
"Prefer explicit `Uploadcare::Client` instances for application code, especially when an app uses more than one Uploadcare project.",
"Configure credentials with `Uploadcare::Client.new(public_key: ..., secret_key: ...)`, `Uploadcare.configure`, or the UPLOADCARE_PUBLIC_KEY and UPLOADCARE_SECRET_KEY environment variables.",
"Never hardcode real Uploadcare API keys in examples or application code; use environment variables or an application secrets store.",
"Use `client.files`, `client.groups`, `client.uploads`, `client.project`, `client.webhooks`, `client.file_metadata`, `client.addons`, and `client.conversions` for normal application workflows.",
"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.",
"Use `client.api.rest` and `client.api.upload` when exact REST API or Upload API endpoint parity is needed.",
"Use `client.uploads.upload` for automatic upload method selection and `client.uploads.multipart_upload` for explicit multipart uploads.",
"Pass `tags: [...]` when uploading, and use `client.file_tags` to list, replace, add, or delete tags on an existing file.",
"Use `MIGRATING_V5.md` when upgrading applications from uploadcare-ruby v4.x to v5."
],
"previousVersions": [
Expand Down
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ mise exec -- ruby examples/simple_upload.rb spec/fixtures/kitten.jpeg
Force multipart upload and show throughput details.
- `examples/url_upload.rb`
Upload a remote URL and show async polling as a follow-up example.
- `examples/file_tags.rb`
Upload a file with tags, then list, replace, add, and delete tags.
- `examples/group_creation.rb`
Upload multiple files, create a group, and print group details.

Expand Down
37 changes: 37 additions & 0 deletions examples/file_tags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative '../lib/uploadcare'
require 'dotenv/load'

file_path = ARGV[0]

unless file_path && File.exist?(file_path)
puts 'Usage: ruby file_tags.rb <file_path>'
puts 'Example: ruby file_tags.rb photo.jpg'
exit 1
end

begin
client = Uploadcare::Client.new(
public_key: ENV.fetch('UPLOADCARE_PUBLIC_KEY'),
secret_key: ENV.fetch('UPLOADCARE_SECRET_KEY')
)

file = File.open(file_path, 'rb') do |io|
client.files.upload(io, store: true, tags: %w[example draft])
end

puts "Uploaded #{file.uuid} with tags: #{client.file_tags.list(uuid: file.uuid).join(', ')}"

change = client.file_tags.replace(uuid: file.uuid, tags: %w[example approved])
puts "Replaced tags: #{change.tags.join(', ')}"

change = client.file_tags.update(uuid: file.uuid, add: ['featured'], delete: ['example'])
puts "Updated tags: #{change.tags.join(', ')}"
rescue StandardError => e
warn "File tag example failed: #{e.message}"
exit 1
ensure
client.api.rest.files.delete(uuid: file.uuid) if file&.uuid
end
2 changes: 2 additions & 0 deletions lib/uploadcare.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ def eager_load!
Webhook = Resources::Webhook
# Alias for the file metadata resource.
FileMetadata = Resources::FileMetadata
# Alias for the file tags resource.
FileTags = Resources::FileTags
# Alias for the add-on execution resource.
AddonExecution = Resources::AddonExecution
# Alias for the document conversion resource.
Expand Down
20 changes: 18 additions & 2 deletions lib/uploadcare/api/rest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

# Base client for the Uploadcare REST API.
#
# Provides authenticated HTTP methods (GET, POST, PUT, DELETE) for all REST API
# Provides authenticated HTTP methods (GET, POST, PUT, PATCH, DELETE) for all REST API
# endpoints. Includes automatic error handling and throttle retry logic.
#
# Endpoint classes are accessed via lazy-loaded accessors:
Expand Down Expand Up @@ -71,6 +71,11 @@ def file_metadata
memoized(:@file_metadata) { Uploadcare::Api::Rest::FileMetadata.new(rest: self) }
end

# @return [Uploadcare::Api::Rest::FileTags] Per-file tag operations endpoint
def file_tags
memoized(:@file_tags) { Uploadcare::Api::Rest::FileTags.new(rest: self) }
end

# @return [Uploadcare::Api::Rest::Addons] Add-on operations endpoint
def addons
memoized(:@addons) { Uploadcare::Api::Rest::Addons.new(rest: self) }
Expand All @@ -90,7 +95,7 @@ def video_conversions

# Make an HTTP request to the REST API.
#
# @param method [Symbol] HTTP method (:get, :post, :put, :delete)
# @param method [Symbol] HTTP method (:get, :post, :put, :patch, :delete)
# @param path [String] API endpoint path
# @param params [Hash, Array, String] Request parameters
# @param headers [Hash] Additional request headers
Expand Down Expand Up @@ -141,6 +146,17 @@ def put(path:, params: {}, headers: {}, request_options: {})
request(method: :put, path: path, params: params, headers: headers, request_options: request_options)
end

# Make a PATCH request wrapped in a Result.
#
# @param path [String] API endpoint path
# @param params [Hash] Request body parameters
# @param headers [Hash] Additional request headers
# @param request_options [Hash] Request options
# @return [Uploadcare::Result]
def patch(path:, params: {}, headers: {}, request_options: {})
request(method: :patch, path: path, params: params, headers: headers, request_options: request_options)
end

# Make a DELETE request wrapped in a Result.
#
# @param path [String] API endpoint path
Expand Down
Loading