Skip to content

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 }
For an admin, 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…" } ] }
Recursive (matches 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 via mock_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.