Storage-svc poskytuje dva BE-proxy endpointy pro správu objektů: list (s prefix filterem a paginací) a delete (jednoho objektu podle key). Na rozdíl od upload/download zde storage-svc volá Cloudflare R2 S3 SDK přímo (BE-proxy, žádný presign) — operace jsou levné, malé responsi, low-latency je důležitější než data-path optimalizace.
- Endpoint hostován v storage-svc (per-namespace).
listpaginace přescursor(R2 native S3ContinuationToken).deleteMVP: single-object only; bulk delete odložen na follow-up.- Operace volá user-app BE (HMAC token v env, ne v browseru).
Flow 1 — List Files
sequenceDiagram
actor UA as User App BE
participant SS as storage-svc
participant CF as Cloudflare R2
UA->>+SS: GET /api/v1/storage/list?prefix=uploads/avatars/&cursor=...&limit=50 <br> X-Talkide-App-Token: {hmac}
SS->>SS: verify HMAC token
alt token invalid
SS-->>UA: 401 Unauthorized <br> ErrorResponse
end
SS->>SS: validate query params (prefix sanity, limit ≤ 1000)
alt invalid
SS-->>UA: 400 Bad Request <br> ErrorResponse
end
SS->>+CF: ListObjectsV2 <br> { Bucket, Prefix, ContinuationToken, MaxKeys }
alt CF error
CF-->>SS: 5xx
SS-->>UA: 502 Bad Gateway <br> ErrorResponse
end
CF->>-SS: { Contents: [...], NextContinuationToken, IsTruncated }
SS->>-UA: 200 OK <br> ListFilesResponse
API Contract
GET /api/v1/storage/list
Query parameters:
| Param | Type | Required | Default | Note |
|---|---|---|---|---|
prefix | string | no | "" (root) | Object key prefix filter |
cursor | string | no | — | Pagination cursor (= R2 ContinuationToken from previous response) |
limit | int | no | 100 | Max 1000 (R2 hard cap) |
Headers:
X-Talkide-App-Token: <hmac>— required.
200 OK ListFilesResponse:
{
"data": {
"items": [
{
"key": "uploads/avatars/user-123.png",
"sizeBytes": 524288,
"lastModified": "2026-05-23T14:30:00Z",
"etag": "9a0364b9e99bb480dd25e1f0284c8555"
},
{
"key": "uploads/avatars/user-456.jpg",
"sizeBytes": 819200,
"lastModified": "2026-05-24T08:15:00Z",
"etag": "5f4dcc3b5aa765d61d8327deb882cf99"
}
],
"nextCursor": "1#!CONTINUATION#!opaque-r2-token-here",
"hasMore": true
}
}
nextCursor je null pokud IsTruncated=false.
Error responses
400 Bad Request (invalid prefix — traversal / forbidden chars) ErrorResponse:
{
"status": 400,
"code": "STORAGE_INVALID_PREFIX",
"message": "Prefix contains forbidden characters"
}
400 Bad Request (limit > 1000) ErrorResponse:
{
"status": 400,
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"errors": [
{ "field": "limit", "message": "must be between 1 and 1000" }
]
}
401 Unauthorized ErrorResponse — viz UC-12002.
502 Bad Gateway (R2 upstream error) ErrorResponse:
{
"status": 502,
"code": "STORAGE_UPSTREAM_ERROR",
"message": "Storage backend temporarily unavailable"
}
Flow 2 — Delete File
sequenceDiagram
actor UA as User App BE
participant SS as storage-svc
participant CF as Cloudflare R2
UA->>+SS: DELETE /api/v1/storage/objects/{key} <br> X-Talkide-App-Token: {hmac}
SS->>SS: verify HMAC token
alt token invalid
SS-->>UA: 401 Unauthorized <br> ErrorResponse
end
SS->>SS: validate + sanitize key
alt invalid
SS-->>UA: 400 Bad Request <br> ErrorResponse
end
SS->>+CF: DeleteObject <br> { Bucket, Key }
alt CF error (5xx)
CF-->>SS: 5xx
SS-->>UA: 502 Bad Gateway <br> ErrorResponse
end
CF->>-SS: 204 No Content (R2 vrací 204 i pro non-existing key — S3 sémantika)
SS->>-UA: 204 No Content
API Contract
DELETE /api/v1/storage/objects/{key} — {key} je URL-encoded object key (např. uploads%2Favatars%2Fuser-123.png).
Pozn.: R2/S3 sémantika: DELETE vrací 204 No Content i pro neexistující key (idempotent). Pokud klient potřebuje vědět, jestli objekt opravdu existoval, musí napřed udělat HEAD (mimo scope MVP).
Headers:
X-Talkide-App-Token: <hmac>— required.
204 No Content — bez body.
Error responses
400 Bad Request (invalid key) ErrorResponse — viz UC-12002.
401 Unauthorized ErrorResponse.
502 Bad Gateway ErrorResponse.
Sanitization (prefix)
prefix má volnější pravidla než key v UC-12002 — povolen prázdný string (root listing) a koncový slash. Stále zakázáno:
..segmenty- leading slash
- control chars / NUL bytes
- length > 1024
fun sanitizePrefix(raw: String?): String {
val s = (raw ?: "").trim()
require(s.length <= 1024) { "STORAGE_INVALID_PREFIX" }
if (s.isEmpty()) return s
require(!s.startsWith("/")) { "STORAGE_INVALID_PREFIX" }
require(!s.contains("..")) { "STORAGE_INVALID_PREFIX" }
require(!s.contains("//")) { "STORAGE_INVALID_PREFIX" }
require(s.none { it.isISOControl() || it == '�' }) { "STORAGE_INVALID_PREFIX" }
return s
}
Frontend
User-app FE volání jde přes user-app BE (proxy).
Validations (user-app SDK)
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| prefix | optional, sanitized | 0 – 1024 chars | viz sanitizePrefix | |
| cursor | optional, opaque | 0 – 2048 chars | — | R2 continuation token, treat as opaque blob |
| limit | optional, positive | 1 – 1000 | — | |
| key (delete) | not_blank, sanitized | 1 – 1024 chars | viz UC-12002 |
Backend (storage-svc)
Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| X-Talkide-App-Token | not_blank, constant-time compare | 64 hex chars | ^[0-9a-f]{64}$ | |
| prefix (query) | optional, sanitized | 0 – 1024 chars | viz sanitizePrefix | |
| cursor (query) | optional | 0 – 2048 chars | — | Pass-through to R2 ContinuationToken |
| limit (query) | optional, positive | 1 – 1000 | — | Default 100 |
| key (path) | not_blank, sanitized | 1 – 1024 chars | viz UC-12002 | URL-decoded before validation |
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
| valid HMAC, prefix=“uploads/”, bucket obsahuje 50 objektů pod prefixem | GET /list je volán | 200 OK; items[] délka ≤ limit; hasMore=false pokud je všech ≤ limit |
| valid HMAC, prefix=“uploads/”, bucket obsahuje 200 objektů, limit=100 | GET /list je volán | 200 OK; items[] délka 100; nextCursor != null; hasMore=true |
valid HMAC, opakované volání s cursor z předchozí response | GET /list je volán | 200 OK; vrátí zbývající objekty; eventuálně nextCursor=null |
| chybí HMAC token | GET /list je volán | 401 AUTHENTICATION_FAILED |
| HMAC token nesprávný | GET /list je volán | 401 AUTHENTICATION_FAILED |
prefix obsahuje .. | GET /list je volán | 400 STORAGE_INVALID_PREFIX |
prefix začíná / | GET /list je volán | 400 STORAGE_INVALID_PREFIX |
| limit=0 | GET /list je volán | 400 VALIDATION_ERROR |
| limit=1001 | GET /list je volán | 400 VALIDATION_ERROR |
| prefix neexistuje v bucketu | GET /list je volán | 200 OK; items=[]; nextCursor=null; hasMore=false |
| R2 ListObjectsV2 vrátí 5xx | GET /list je volán | 502 STORAGE_UPSTREAM_ERROR; log WARN |
| empty prefix (root listing) | GET /list je volán | 200 OK; items[] obsahuje top-level objekty bucketu |
| valid HMAC, key existuje | DELETE /objects/{key} je volán | 204 No Content; R2 DeleteObject zavolán |
| valid HMAC, key NEexistuje | DELETE /objects/{key} je volán | 204 No Content (R2 vrací 204 pro non-existing — idempotent) |
| chybí HMAC token | DELETE /objects/{key} je volán | 401 AUTHENTICATION_FAILED |
key v path obsahuje URL-encoded .. | DELETE /objects/{key} je volán | 400 STORAGE_INVALID_KEY |
key obsahuje URL-encoded / (slash) | DELETE /objects/{key} je volán | 204 No Content (slash je validní jako oddělovač “složek” v object key) |
| R2 DeleteObject vrátí 5xx | DELETE /objects/{key} je volán | 502 STORAGE_UPSTREAM_ERROR |
| delete pak ihned list se stejným prefixem | DELETE + GET /list | objekt nemá být v items[] (eventual consistency R2 je strong-read after write pro DELETE — viz Cloudflare docs) |
Quota update consideration
DELETE objektu by měl updatovat app_storage_config.bytes_used v platform DB. Storage-svc nedrží authoritative state, takže:
- Option A (MVP): Storage-svc po úspěšném DeleteObject pošle async POST na platform internal endpoint
POST /internal/storage/usage-delta. Best-effort, no retry — případný drift dorovná reconciliation job v UC-12006. - Option B (deferred): Storage-svc neposílá nic, plně se spoléhá na reconciliation job.
Vybrán Option A — minimální cena, drift se zmenší. Pokud platform call selže, log WARN a pokračovat (delete úspěšný, jen quota tracking miss).
POST /internal/storage/usage-delta:
{ "bucketName": "talkide-app-popelkam-todo-list", "deltaBytes": -524288 }
Out of scope (this UC)
- Bulk delete (multiple keys / via list result) — follow-up.
- Move / rename (S3 sémantika: copy + delete) — follow-up.
- Soft delete / versioning — R2 podporuje, ale komplikuje quota tracking; deferred.
- Public list listing (no auth) — záměrně NE.
Thanks for the feedback.