Cloud Storage (GCS)¶
Cloudbox emulates the GCS JSON API v1. The google-cloud-storage Python SDK works against it
without modification — point it at http://localhost:4443 and supply any credentials.
Connection¶
Port: 4443 (override with CLOUDBOX_GCS_PORT)
from google.auth.credentials import AnonymousCredentials
from google.cloud import storage
client = storage.Client(
project="local-project",
credentials=AnonymousCredentials(),
client_options={"api_endpoint": "http://localhost:4443"},
)
Or with the gsutillocal CLI shim (installed after uv sync):
Buckets¶
Create bucket¶
Returns the bucket metadata. Fails with 409 if the bucket already exists.
Optional fields accepted in the body: storageClass, location, labels, cors,
lifecycle, retentionPolicy.
Get bucket¶
Returns bucket metadata. 404 if not found.
List buckets¶
Returns { "kind": "storage#buckets", "items": [...] }.
Patch bucket¶
Updates mutable fields: lifecycle, labels, storageClass, location, cors,
retentionPolicy. Increments metageneration.
Delete bucket¶
204 on success. 409 if the bucket is not empty. 404 if not found.
Objects — uploads¶
Simple (media) upload¶
POST /upload/storage/v1/b/{bucket}/o?uploadType=media&name={object}
Content-Type: image/png
<raw bytes>
Returns object metadata. Object name is required as a query parameter.
Multipart upload¶
POST /upload/storage/v1/b/{bucket}/o?uploadType=multipart
Content-Type: multipart/related; boundary=foo
--foo
Content-Type: application/json
{"name": "my-object", "contentType": "image/png"}
--foo
Content-Type: image/png
<raw bytes>
--foo--
The metadata part (first section) may contain name and contentType. The object name
may also be supplied as a name query parameter.
Resumable upload¶
Initiate:
POST /upload/storage/v1/b/{bucket}/o?uploadType=resumable&name={object}
X-Upload-Content-Type: video/mp4
X-Upload-Content-Length: 10485760
Returns 200 with a Location header pointing to the upload session URL
(?uploadType=resumable&upload_id=<id>).
Upload chunk (PUT to session URL):
PUT /upload/storage/v1/b/{bucket}/o?uploadType=resumable&upload_id={id}
Content-Range: bytes 0-999999/10485760
<chunk bytes>
Returns 308 Resume Incomplete with a Range header while more data is expected,
or object metadata when the upload is complete.
Query status:
Send a PUT with an empty body and Content-Range: bytes */{total} to get a 308
response that includes the Range header showing how many bytes have been received.
Upload preconditions¶
All upload types accept ifGenerationMatch as a query parameter. Pass "0" to
require that the object does not already exist.
Objects — download¶
Download via metadata endpoint¶
Returns the object body with its stored Content-Type.
Download via dedicated path¶
Equivalent to alt=media above. Both paths support byte-range requests.
Byte-range download¶
Include an HTTP Range header:
Range: bytes=0-499 # first 500 bytes
Range: bytes=500- # from byte 500 to end
Range: bytes=-100 # last 100 bytes
Returns 206 Partial Content with a Content-Range header. Returns 416 if the range
is unsatisfiable.
Objects — metadata¶
Get object metadata¶
Returns the full object metadata JSON (without alt=media).
Patch object metadata¶
Updates mutable fields: contentType, metadata, contentDisposition, cacheControl,
contentEncoding, temporaryHold, eventBasedHold. Increments metageneration. Fires
OBJECT_METADATA_UPDATE notification.
List objects¶
Query parameters:
| Parameter | Default | Description |
|---|---|---|
prefix |
"" |
Filter to objects whose name begins with this value |
delimiter |
"" |
Collapse names at this delimiter into prefixes entries |
maxResults |
1000 |
Maximum objects per page |
pageToken |
"" |
Opaque token from a previous response for pagination |
Returns:
Delete object¶
204 on success. 403 if the object is within a retention period. Fires OBJECT_DELETE
notification.
Object operations¶
Copy object¶
Server-side copy. Returns the destination object metadata. Both the source object and destination bucket must exist.
Compose objects¶
{
"sourceObjects": [
{ "name": "part-1" },
{ "name": "part-2", "objectPreconditions": { "ifGenerationMatch": "3" } }
],
"destination": { "contentType": "application/octet-stream" }
}
Concatenates up to 32 source objects in order. Optional ifGenerationMatch per source
enforces a generation precondition. Returns destination object metadata.
Rewrite object¶
Copies an object with optional metadata overrides (contentType, storageClass).
Completes in a single request — no rewriteToken polling required.
Response shape:
{
"kind": "storage#rewriteResponse",
"done": true,
"totalBytesRewritten": "1024",
"objectSize": "1024",
"resource": { ... }
}
Preconditions¶
All object read, write, and delete operations accept conditional request headers and query parameters:
| Mechanism | Header / Parameter | Description |
|---|---|---|
| ETag match | If-Match: <etag> |
Fail unless etag matches |
| ETag non-match | If-None-Match: <etag> |
Fail (304) if etag matches |
| Generation | ifGenerationMatch=<n> |
Fail unless generation equals n; "0" requires absence |
| Metageneration | ifMetagenerationMatch=<n> |
Fail unless metageneration equals n |
Precondition failures return 412 Precondition Failed (or 304 Not Modified for
If-None-Match).
Checksums¶
Every stored object receives automatically computed checksums:
md5Hash— MD5 digest of the object body, base64-encoded.crc32c— CRC32c checksum, base64-encoded.etag— Same value asmd5Hash.
These are returned in object metadata and can be used for integrity verification.
CORS configuration¶
Get CORS¶
Set CORS¶
{
"cors": [
{
"origin": ["https://example.com"],
"method": ["GET", "PUT"],
"responseHeader": ["Content-Type"],
"maxAgeSeconds": 3600
}
]
}
Replaces the entire CORS configuration for the bucket.
Delete CORS¶
Clears all CORS rules. Returns 204.
Retention policies¶
Get retention policy¶
Set / update retention policy¶
retentionPeriod is in seconds. Once set, objects in the bucket cannot be deleted until
their retentionExpirationTime has passed. The expiry is computed at upload time and stored
on the object.
Delete retention policy¶
Fails with 403 if the policy has been locked (isLocked: true).
Lifecycle rules¶
Set lifecycle rules when creating or patching a bucket:
{
"lifecycle": {
"rule": [
{
"action": { "type": "Delete" },
"condition": { "age": 30 }
},
{
"action": { "type": "SetStorageClass", "storageClass": "COLDLINE" },
"condition": { "createdBefore": "2024-01-01", "matchesStorageClass": ["STANDARD"] }
}
]
}
}
Lifecycle rules are evaluated lazily on each list request. Supported conditions:
| Condition | Description |
|---|---|
age |
Object age in days since timeCreated |
createdBefore |
RFC 3339 date; objects created before this date match |
matchesStorageClass |
List of storage class names to match |
Supported actions: Delete, SetStorageClass.
Pub/Sub notifications¶
Buckets can be configured to publish messages to a Pub/Sub topic when object events occur. The Pub/Sub topic must exist in the Cloudbox Pub/Sub emulator.
Create notification config¶
{
"topic": "projects/local-project/topics/my-topic",
"payload_format": "JSON_API_V1",
"event_types": ["OBJECT_FINALIZE", "OBJECT_DELETE"],
"object_name_prefix": "uploads/",
"custom_attributes": { "env": "local" }
}
| Field | Description |
|---|---|
topic |
Full Pub/Sub topic resource name |
payload_format |
"JSON_API_V1" (full object metadata) or "NONE" (empty payload) |
event_types |
List of event types to fire on; omit to fire on all events |
object_name_prefix |
Only fire for objects with this name prefix |
custom_attributes |
Key-value pairs added to message attributes |
Event types: OBJECT_FINALIZE, OBJECT_DELETE, OBJECT_METADATA_UPDATE, OBJECT_ARCHIVE.
List notification configs¶
Get notification config¶
Delete notification config¶
Object metadata fields¶
| Field | Type | Description |
|---|---|---|
name |
string | Object name within the bucket |
bucket |
string | Bucket name |
generation |
string | Integer generation number (increments on each overwrite) |
metageneration |
string | Integer metadata generation (increments on metadata update) |
contentType |
string | MIME type |
size |
string | Object size in bytes |
md5Hash |
string | Base64-encoded MD5 of the body |
crc32c |
string | Base64-encoded CRC32c of the body |
etag |
string | Same as md5Hash |
timeCreated |
string | RFC 3339 creation timestamp (preserved across overwrites) |
updated |
string | RFC 3339 last-modified timestamp |
storageClass |
string | Storage class (default: STANDARD) |
retentionExpirationTime |
string | RFC 3339 expiry from bucket retention policy, if set |
metadata |
object | Arbitrary user-defined key-value pairs |
contentDisposition |
string | Content-Disposition header value |
cacheControl |
string | Cache-Control header value |
contentEncoding |
string | Content-Encoding header value |
temporaryHold |
bool | When true, blocks deletion until cleared |
eventBasedHold |
bool | When true, blocks deletion until cleared |
Signed URLs¶
Cloudbox supports signed URL generation and validation via a Cloudbox-specific endpoint.
Real GCS signed URLs are generated client-side using service account credentials.
Because the emulator uses AnonymousCredentials, a server-side signing endpoint is provided instead.
Generate a signed URL¶
{
"bucket": "my-bucket",
"object": "path/to/file.txt",
"method": "GET",
"expiration": 3600,
"contentType": "text/plain"
}
method defaults to "GET". expiration is in seconds from now (default 3600).
contentType is optional.
Response:
{
"signedUrl": "http://localhost:4443/my-bucket/path/to/file.txt?X-Goog-Algorithm=GOOG4-HMAC-SHA256&..."
}
Use a signed URL¶
Signed URLs target the XML API path /{bucket}/{object}:
| Method | Description |
|---|---|
GET /{bucket}/{object} |
Download the object body |
PUT /{bucket}/{object} |
Upload (replace) the object body |
DELETE /{bucket}/{object} |
Delete the object |
All requests must carry X-Goog-Algorithm, X-Goog-Date, X-Goog-Expires, and
X-Goog-Signature query parameters. Requests without these parameters are rejected with 403.
Cloudbox validates:
- The URL has not expired (X-Goog-Date + X-Goog-Expires ≥ now)
- The HMAC-SHA256 signature matches (for GOOG4-HMAC-SHA256 URLs generated by this endpoint)
Signed URLs generated by the real GCS SDK (GOOG4-RSA-SHA256) are accepted as long as
the expiry is valid — the RSA signature is not verified (the emulator does not have the
service account private key).
import httpx, requests
# Generate
r = requests.post("http://localhost:4443/_cloudbox/sign", json={
"bucket": "my-bucket", "object": "hello.txt", "method": "GET", "expiration": 300,
})
url = r.json()["signedUrl"]
# Use (no auth needed)
data = requests.get(url).content
Known limitations¶
The following GCS features are not emulated:
| Feature | Notes |
|---|---|
| Object versioning | Only the latest generation of each object is retained |
ACLs (objectAccessControls, bucketAccessControls) |
No access control enforcement |
IAM (getIamPolicy / setIamPolicy) |
Endpoints not implemented |
| XML API (S3-compatible) | Only the JSON API and the signed-URL XML routes are supported |
| Customer-managed encryption keys (CMEK) | Objects are stored in plaintext |
| Object holds — release-on-event semantics | Holds block deletion; the emulator does not model "event" release triggers — clear holds explicitly via PATCH |
| Uniform bucket-level access | Not enforced |
Examples¶
# Run all GCS examples (requires Cloudbox running on port 4443)
uv run python examples/gcs/upload_download.py
uv run python examples/gcs/compose.py
uv run python examples/gcs/byte_range.py
uv run python examples/gcs/cors.py
uv run python examples/gcs/retention.py
| Example | What it demonstrates |
|---|---|
upload_download.py |
Media, multipart, and resumable uploads; download; metadata patch; delete |
compose.py |
Composing multiple objects into one; generation preconditions |
byte_range.py |
Partial content requests with Range header; 416 on bad ranges |
cors.py |
Setting, reading, updating, and clearing CORS rules |
retention.py |
Retention policies; locked policies; delete blocked within retention window |