Storage API¶
Per-user file storage on the platform, backed by S3/MinIO. Every user is boxed
into a private users/{user_id}/ prefix; a user can only touch their own
prefix, admins are exempt. All endpoints require a bearer token
(Authorization: Bearer <jwt|gw_…>); user_id is the token's sub.
Base path: /v1/storage. Interactive schema at /docs.
Endpoints¶
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/storage/status |
Object-store health (used %, watermark, degraded) — drives the webui banner |
POST |
/v1/storage/upload |
Upload a file into the caller's prefix (multipart, streamed) |
GET |
/v1/storage/usage |
Caller's used/quota bytes; admins also get the per-user table |
GET |
/v1/storage/list |
List the caller's files (recursive); admin lists the whole bucket |
POST |
/v1/storage/download |
Download a file by s3:// URI (own prefix only, admin exempt) |
POST |
/v1/storage/delete |
Delete a file by s3:// URI (own prefix only, admin exempt) |
GET /v1/storage/status¶
{ "used_pct": 41.2, "watermark_pct": 92.0, "degraded": false }
degraded is true once used_pct >= watermark_pct; uploads are then refused
(see 507 below).
POST /v1/storage/upload¶
Multipart form with a file field. Optional dest form field sets a
sub-path (e.g. models/qwen/weights.bin). Streamed in 8 MiB chunks.
{ "uri": "s3://gridweave-artifacts/users/alice/weights.bin", "size": 12345 }
GET /v1/storage/usage¶
{ "used_bytes": 12345, "quota_bytes": 21474836480, "unlimited": false }
unlimited is true and the response also carries a
per-user table sorted by usage:
{ "used_bytes": 0, "quota_bytes": …, "unlimited": true,
"users": [ { "user_id": "alice", "used_bytes": 999 }, … ] }
GET /v1/storage/list?prefix=<optional>¶
{ "files": [ { "uri": "s3://…/users/alice/a.bin", "size": 10, "last_modified": "2026-07-01T…" } ] }
usage), so files in sub-folders are always listed.
POST /v1/storage/download · POST /v1/storage/delete¶
Body: { "uri": "s3://…" }. Download streams the object as
application/octet-stream; delete removes a single object.
Error codes¶
| Code | When |
|---|---|
403 |
uri/prefix is outside the caller's users/{sub}/ prefix (non-admin) |
404 |
file not found on download/delete |
413 |
upload would exceed the caller's quota (USER_STORAGE_QUOTA_GB) |
507 |
object store is at/above the watermark — uploads temporarily refused |
Configuration¶
| Env var | Default | Meaning |
|---|---|---|
S3_ENDPOINT |
http://minio:9000 |
Object-store endpoint |
S3_ACCESS_KEY / S3_SECRET_KEY |
gridweave / — |
Credentials |
S3_BUCKET |
gridweave-artifacts |
Bucket holding users/{id}/… prefixes |
USER_STORAGE_QUOTA_GB |
20 |
Per-user quota (admins exempt) |
STORAGE_WATERMARK_PCT |
92 |
Fill % at which uploads start returning 507 |
Tests¶
tests/platform_service/test_routers_storage.py— upload/download/list/delete with per-user prefix isolation + admin bypass (S3 mocked viamock_s3).tests/platform_service/test_storage_quota.py— 413 quota enforcement.tests/platform_service/test_storage_watermark.py— 507 watermark / fail-open.
Run: pytest tests/platform_service/test_routers_storage.py tests/platform_service/test_storage_quota.py tests/platform_service/test_storage_watermark.py.
Live MinIO integration is exercised in the Layer 3 cluster pass.