Internal Documentation internal
TalkIDE internal documentation

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).
  • list paginace přes cursor (R2 native S3 ContinuationToken).
  • delete MVP: 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:

ParamTypeRequiredDefaultNote
prefixstringno"" (root)Object key prefix filter
cursorstringnoPagination cursor (= R2 ContinuationToken from previous response)
limitintno100Max 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)

FieldConstraintsSizePatternNote
prefixoptional, sanitized0 – 1024 charsviz sanitizePrefix
cursoroptional, opaque0 – 2048 charsR2 continuation token, treat as opaque blob
limitoptional, positive1 – 1000
key (delete)not_blank, sanitized1 – 1024 charsviz UC-12002

Backend (storage-svc)

Validations

FieldConstraintsSizePatternNote
X-Talkide-App-Tokennot_blank, constant-time compare64 hex chars^[0-9a-f]{64}$
prefix (query)optional, sanitized0 – 1024 charsviz sanitizePrefix
cursor (query)optional0 – 2048 charsPass-through to R2 ContinuationToken
limit (query)optional, positive1 – 1000Default 100
key (path)not_blank, sanitized1 – 1024 charsviz UC-12002URL-decoded before validation

Test Cases

GIVENWHENTHEN
valid HMAC, prefix=“uploads/”, bucket obsahuje 50 objektů pod prefixemGET /list je volán200 OK; items[] délka ≤ limit; hasMore=false pokud je všech ≤ limit
valid HMAC, prefix=“uploads/”, bucket obsahuje 200 objektů, limit=100GET /list je volán200 OK; items[] délka 100; nextCursor != null; hasMore=true
valid HMAC, opakované volání s cursor z předchozí responseGET /list je volán200 OK; vrátí zbývající objekty; eventuálně nextCursor=null
chybí HMAC tokenGET /list je volán401 AUTHENTICATION_FAILED
HMAC token nesprávnýGET /list je volán401 AUTHENTICATION_FAILED
prefix obsahuje ..GET /list je volán400 STORAGE_INVALID_PREFIX
prefix začíná /GET /list je volán400 STORAGE_INVALID_PREFIX
limit=0GET /list je volán400 VALIDATION_ERROR
limit=1001GET /list je volán400 VALIDATION_ERROR
prefix neexistuje v bucketuGET /list je volán200 OK; items=[]; nextCursor=null; hasMore=false
R2 ListObjectsV2 vrátí 5xxGET /list je volán502 STORAGE_UPSTREAM_ERROR; log WARN
empty prefix (root listing)GET /list je volán200 OK; items[] obsahuje top-level objekty bucketu
valid HMAC, key existujeDELETE /objects/{key} je volán204 No Content; R2 DeleteObject zavolán
valid HMAC, key NEexistujeDELETE /objects/{key} je volán204 No Content (R2 vrací 204 pro non-existing — idempotent)
chybí HMAC tokenDELETE /objects/{key} je volán401 AUTHENTICATION_FAILED
key v path obsahuje URL-encoded ..DELETE /objects/{key} je volán400 STORAGE_INVALID_KEY
key obsahuje URL-encoded / (slash)DELETE /objects/{key} je volán204 No Content (slash je validní jako oddělovač “složek” v object key)
R2 DeleteObject vrátí 5xxDELETE /objects/{key} je volán502 STORAGE_UPSTREAM_ERROR
delete pak ihned list se stejným prefixemDELETE + GET /listobjekt 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.

Was this page helpful?

Thanks for the feedback.