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