Blob storage
Arquivos não cabem no estado do pipeline: ele é sempre um documento JSON, e o corpo de um arquivo
não é JSON. Os steps storage.* existem para isso — e a decisão de desenho mais importante deles é
que o conteúdo do arquivo nunca vira estado do pipeline, em nenhum dos modos.
Declarando um storage
spec:
storages:
- id: docs
provider: s3
config:
endpoint: "${config:Storage:Endpoint}"
bucket: docs
accessKeyId: "${secret:env:STORAGE_ACCESS_KEY}"
secretAccessKey: "${secret:env:STORAGE_SECRET_KEY}"
forcePathStyle: true # tipicamente exigido por MinIO/RustFS
Um único provider: s3 cobre AWS S3, MinIO e RustFS: os três falam o mesmo protocolo, e o que
muda é o endpoint e o forcePathStyle. Um backend com protocolo genuinamente diferente (Azure
Blob, GCS) entra como um pacote próprio sob a própria chave — veja
IStorageProvider.
As credenciais precisam ser ${secret:...}; um literal no YAML é recusado no startup. Esse é um
dos poucos campos do framework onde a exigência é imposta de verdade, não apenas recomendada.
Dois modos de transferência
| Modo | Steps | O corpo do arquivo passa pela sua API? |
|---|---|---|
| URL pré-assinada | storage.presignUpload, storage.presignDownload |
Não — o cliente transfere direto com o backend |
| Streaming | storage.put, storage.get |
Sim, mas em streaming: nunca é materializado na memória |
Os dois são suportados de propósito, porque resolvem problemas diferentes.
URL pré-assinada é o caminho preferido quando o arquivo é grande ou o volume é alto: sua API só
gera uma URL temporária e sai da frente do tráfego. Funciona em qualquer tipo de aplicação, inclusive
num consumer, porque é operação puramente de metadata.
Streaming é o caminho quando a transferência precisa passar pela API de qualquer forma — para
aplicar autorização por requisição, por exemplo. Aqui há uma restrição real: storage.put e
storage.get só existem em spec.type: api. Dentro do pipeline de um consumer eles são
recusados no build, não em runtime, porque ali não existe corpo de requisição/resposta HTTP para
transmitir.
O fluxo típico: upload por URL pré-assinada
- id: iniciarUpload
method: POST
path: /docs
pipeline:
- { type: storage.presignUpload, id: upload, storage: docs, key: "'docs/' & $uuid()", ttl: 5m }
response:
status: 200
body: "steps.upload.result" # { key, url, expiresAt }
O cliente recebe { key, url, expiresAt }, faz o PUT direto na url, e depois confirma:
- id: confirmarUpload
method: POST
path: /docs/{key}/confirmar
pipeline:
- { type: storage.stat, id: meta, storage: docs, key: input.route.key }
- { type: fail, when: "steps.meta.result = null", status: 404, code: UPLOAD_NAO_ENCONTRADO }
- { type: db.persist, entity: Documento, data: "{ 'key': input.route.key, 'size': steps.meta.result.size }" }
errors:
- { code: UPLOAD_NAO_ENCONTRADO, status: 404, title: "Upload não encontrado" }
storage.stat é a forma barata de confirmar que o arquivo chegou: lê os metadados sem transferir o
conteúdo. Gerar a URL pré-assinada não verifica se o objeto existe — é só uma assinatura — então
essa confirmação é sua.
O que cada step publica
Todos os storage.* que produzem resultado exigem id:: sem ele, o build falha.
| Step | Publica em steps.<id>.result |
|---|---|
storage.put |
{ key, size, etag } |
storage.get |
Os metadados do objeto, ou null se não existe (o conteúdo vai para a resposta HTTP) |
storage.stat |
Os metadados, ou null se não existe |
storage.list |
Array de { key, size, lastModified } |
storage.presignUpload / storage.presignDownload |
{ key, url, expiresAt } |
storage.delete |
Nada |
Note storage.get: o que vai para steps.<id>.result são só os metadados. O conteúdo é
transmitido direto para o corpo da resposta HTTP — nunca passa pelo estado do pipeline.
Objeto inexistente não é falha em storage.get e storage.stat: publica null, e o padrão para
virar 404 é o mesmo do db.find — um fail com when logo em seguida, como no exemplo acima.
Sem fail-open
Diferente do cache, storage não tem comportamento fail-open: se o backend estiver indisponível,
a requisição falha. É a escolha certa — cache é uma otimização, storage é o dado. Os códigos de
falha (storage_put_failed, storage_get_failed, ...) estão no
catálogo de códigos de erro, e podem
ser remapeados pela tabela errors: da rota como qualquer outro.
Para onde ir daqui
- Referência de steps — os sete
storage.*, com exemplo e contrato completo de cada um. spec.storages— todos os campos de configuração.- Tratamento de erros — como as falhas acima viram resposta HTTP.
IStorageProvider— para adicionar um backend próprio.