Object Operations
The Oyster S3 API supports these object operations:
- PutObject: Uploads an object to a bucket
- GetObject: Downloads an object's contents
- HeadObject: Retrieves object metadata without downloading the contents
- DeleteObject: Deletes an object from a bucket
- ListObjectsV2: Lists objects in a bucket with optional filtering and pagination
- Object tagging: Sets, reads, and removes an object's tags
All S3 operations require authentication (SigV4), including reads. See S3 Setup for configuration.
Authenticated reads: Unlike the JSON API, where blob reads are public and unauthenticated, all S3 reads require authentication.
PutObject
Uploads an object to a bucket. If an object with the same key already exists, the upload replaces it. PutObject accepts single-part uploads up to 1GB (see Maximum object size).
aws --profile oyster s3api put-object \
--bucket my-bucket \
--key hello.txt \
--body hello.txt
Response:
{
"ETag": "\"9a0364b9e99bb480dd25e1f0284c8555\""
}
The ETag is the MD5 digest of the uploaded content.
How to set Content-Type
aws --profile oyster s3api put-object \
--bucket my-bucket \
--key image.png \
--body photo.png \
--content-type "image/png"
If you omit --content-type, it defaults to application/octet-stream.
How to set tags on upload
Attach tags at upload time with --tagging, a URL-encoded query string of
key=value pairs:
aws --profile oyster s3api put-object \
--bucket my-bucket \
--key hello.txt \
--body hello.txt \
--tagging "env=prod&team=platform"
Tags set this way share the same store as the JSON API and the Object Tagging operations below, and are subject to the same tag rules.
Key behavior
- Overwrite: Uploading to an existing key replaces the object.
- Expiration: Objects share the owning account's
StoragePoollifetime; the background extension service renews the pool before it expires (see Blob Lifecycle). - Content-addressed: Identical content produces the same blob ID internally, enabling deduplication.
Maximum object size
PutObject accepts single-part uploads only, up to a maximum of 1GB per object. Oyster does not support multipart upload, so you cannot split a larger object into parts. Because there is no multipart upload, the ETag is always the MD5 digest of the full object content, even for large objects.
Conditional headers
PutObject supports If-Match and If-None-Match headers for safe writes:
If-None-Match: *: Upload only if the key doesn't already exist (create-only semantics). Returns412 PreconditionFailedif the key exists.If-Match: "<etag>": Overwrite only if the current object's ETag matches. Returns412 PreconditionFailedon mismatch.
# Create-only: fail if the key already exists
aws --profile oyster s3api put-object \
--bucket my-bucket \
--key hello.txt \
--body hello.txt \
--if-none-match "*"
Errors:
| S3 error code | Condition |
|---|---|
NoSuchBucket | Bucket doesn't exist |
PreconditionFailed | If-Match or If-None-Match condition not met |
GetObject
Downloads an object's contents.
aws --profile oyster s3api get-object \
--bucket my-bucket \
--key hello.txt \
downloaded.txt
Response metadata:
{
"ContentLength": 14,
"ContentType": "text/plain",
"ETag": "\"9a0364b9e99bb480dd25e1f0284c8555\"",
"LastModified": "2025-01-15T10:31:00Z"
}
The command writes the file contents to the output path (downloaded.txt in
this example).
Conditional headers
GetObject supports If-Match and If-None-Match for cache validation:
If-Match: "<etag>": Return the object only if its ETag matches. Returns412 PreconditionFailedon mismatch.If-None-Match: "<etag>": Return the object only if its ETag differs. Returns304 NotModifiedif the ETag matches (useful for cache validation).
# Only download if the object has changed
aws --profile oyster s3api get-object \
--bucket my-bucket \
--key hello.txt \
--if-none-match '"9a0364b9e99bb480dd25e1f0284c8555"' \
downloaded.txt
Errors:
| S3 error code | Condition |
|---|---|
NoSuchBucket | Bucket doesn't exist |
NoSuchKey | Object key doesn't exist |
PreconditionFailed | If-Match condition not met |
NotModified | If-None-Match matched; object unchanged (304) |
HeadObject
Retrieves object metadata without downloading the contents. Useful for checking if an object exists or reading its size and content type.
aws --profile oyster s3api head-object \
--bucket my-bucket \
--key hello.txt
Response:
{
"ContentLength": 14,
"ContentType": "text/plain",
"ETag": "\"9a0364b9e99bb480dd25e1f0284c8555\"",
"LastModified": "2025-01-15T10:31:00Z"
}
HeadObject supports the same If-Match and If-None-Match conditional
headers as GetObject. Returns 412 PreconditionFailed or
304 NotModified as appropriate.
Errors:
| S3 error code | Condition |
|---|---|
NoSuchBucket | Bucket doesn't exist |
NoSuchKey | Object key doesn't exist |
PreconditionFailed | If-Match condition not met |
NotModified | If-None-Match matched; object unchanged (304) |
DeleteObject
Deletes an object from a bucket.
aws --profile oyster s3api delete-object \
--bucket my-bucket \
--key hello.txt
Returns no output on success.
This operation is idempotent: deleting a key that doesn't exist still returns success, matching standard S3 behavior.
Deletion is reference-counted: Oyster removes the underlying blob data from storage only when no other keys reference the same content.
Conditional headers
DeleteObject supports If-Match for safe deletion:
If-Match: "<etag>": Delete only if the object's ETag matches. Returns412 PreconditionFailedon mismatch.
Errors:
| S3 error code | Condition |
|---|---|
NoSuchBucket | Bucket doesn't exist |
PreconditionFailed | If-Match condition not met |
ListObjectsV2
Lists objects in a bucket with optional filtering and pagination.
Basic listing
aws --profile oyster s3api list-objects-v2 --bucket my-bucket
Response:
{
"Name": "my-bucket",
"Contents": [
{
"Key": "hello.txt",
"Size": 14,
"ETag": "\"9a0364b9e99bb480...\"",
"LastModified": "2025-01-15T10:31:00Z",
"StorageClass": "STANDARD"
},
{
"Key": "images/photo.png",
"Size": 204800,
"ETag": "\"d41d8cd98f00b204...\"",
"LastModified": "2025-01-15T11:00:00Z",
"StorageClass": "STANDARD"
}
],
"KeyCount": 2,
"MaxKeys": 1000,
"IsTruncated": false
}
How to filter by prefix
List only objects under a specific virtual folder:
aws --profile oyster s3api list-objects-v2 \
--bucket my-bucket \
--prefix "images/"
How to simulate folders with a delimiter
Use --delimiter "/" to group objects into virtual folders:
aws --profile oyster s3api list-objects-v2 \
--bucket my-bucket \
--delimiter "/"
Response:
{
"Name": "my-bucket",
"Contents": [
{
"Key": "hello.txt",
"Size": 14,
"ETag": "\"9a0364b9e99bb480...\"",
"LastModified": "2025-01-15T10:31:00Z",
"StorageClass": "STANDARD"
}
],
"CommonPrefixes": [
{
"Prefix": "images/"
}
],
"KeyCount": 2,
"MaxKeys": 1000,
"Delimiter": "/",
"IsTruncated": false
}
Objects directly at the root level appear in Contents, while virtual folders
(key prefixes before the delimiter) appear in CommonPrefixes.
How to combine prefix and delimiter
List the contents of a specific virtual folder:
aws --profile oyster s3api list-objects-v2 \
--bucket my-bucket \
--prefix "images/" \
--delimiter "/"
How to paginate results
Limit results and paginate through large listings:
# First page
aws --profile oyster s3api list-objects-v2 \
--bucket my-bucket \
--max-keys 10
# Next page (using NextContinuationToken from previous response)
aws --profile oyster s3api list-objects-v2 \
--bucket my-bucket \
--max-keys 10 \
--starting-token "last-key-from-previous-page"
Supported parameters
| Parameter | AWS CLI flag | Description |
|---|---|---|
| Prefix | --prefix | Filter keys that start with this string |
| Delimiter | --delimiter | Group keys by this separator (for example, /) |
| MaxKeys | --max-keys | Maximum objects to return (default: 1000) |
| StartAfter | --start-after | Return keys after this value (lexicographic) |
| ContinuationToken | --starting-token | Continue from a previous response |
Errors:
| S3 error code | Condition |
|---|---|
NoSuchBucket | Bucket doesn't exist |
Object tagging
Oyster implements the three S3 object-tagging operations. Oyster stores tags in
its database in the same blob_tags table that the
JSON API tag endpoints use; a tag set through S3
is visible through the JSON API and vice versa. The same
tag rules apply (maximum 10 tags; key ≤ 128 B;
value ≤ 256 B; set ≤ 2048 B; restricted charset).
PutObjectTagging
Replaces the object's entire tag set.
aws --profile oyster s3api put-object-tagging \
--bucket my-bucket \
--key hello.txt \
--tagging 'TagSet=[{Key=env,Value=prod},{Key=team,Value=platform}]'
GetObjectTagging
Returns the object's current tags.
aws --profile oyster s3api get-object-tagging \
--bucket my-bucket \
--key hello.txt
Response:
{
"TagSet": [
{ "Key": "env", "Value": "prod" },
{ "Key": "team", "Value": "platform" }
]
}
DeleteObjectTagging
Removes all tags from the object.
aws --profile oyster s3api delete-object-tagging \
--bucket my-bucket \
--key hello.txt
Errors:
| S3 error code | Condition |
|---|---|
NoSuchBucket | Bucket doesn't exist |
NoSuchKey | Object key doesn't exist |
DeleteObjectTagging on a missing key
Unlike DeleteObject, DeleteObjectTagging is not idempotent for missing keys. If the object key doesn't exist, the operation returns NoSuchKey. If the bucket doesn't exist, it returns NoSuchBucket.