DevGuideTK
← Trilha Backend

Armazenamento de objetos

Onde guardar arquivos, com URLs assinadas e envio direto do navegador.

O código desta página foi executado nas cinco linguagens com os SDKs oficiais da AWS, contra um SeaweedFS 4.53 compatível com a API do S3. Cada URL gerada foi testada com curl.

URLs de envio e de download, em cada linguagem

Cada versão tem duas funções: urlDeEnvio, que assina um PUT com o tipo do arquivo, e urlDeDownload, que assina um GET com a resposta forçada como anexo. As credenciais vêm das variáveis de ambiente, como o SDK espera; em produção na nuvem, viriam do papel (IAM) da instância ou do contêiner.

import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

// Na AWS, endpoint e forcePathStyle saem; o SDK lê as credenciais do ambiente
const s3 = new S3Client({
  endpoint: process.env.S3_ENDPOINT,
  forcePathStyle: true,
  region: "us-east-1",
  // Sem isto, o SDK põe na URL o checksum de um corpo vazio, e todo envio com conteúdo falha
  requestChecksumCalculation: "WHEN_REQUIRED",
});
const BUCKET = "arquivos";

// O navegador envia direto para o armazenamento, sem passar pela aplicação.
// O tipo entra na assinatura: um envio com outro tipo é recusado.
export const urlDeEnvio = (chave: string, tipo: string, segundos = 300) =>
  getSignedUrl(s3, new PutObjectCommand({ Bucket: BUCKET, Key: chave, ContentType: tipo }), {
    expiresIn: segundos,
    signableHeaders: new Set(["content-type"]), // sem isto, o tipo não entra na assinatura
  });

// O download sai como anexo: o navegador não abre o arquivo como página do seu domínio
export const urlDeDownload = (chave: string, segundos = 300) =>
  getSignedUrl(s3, new GetObjectCommand({
    Bucket: BUCKET, Key: chave,
    ResponseContentDisposition: "attachment",
    ResponseContentType: "application/octet-stream",
  }), { expiresIn: segundos });

console.log(await urlDeEnvio("fotos/ts.png", "image/png"));
console.log(await urlDeDownload("fotos/ts.png"));

O resultado, nas cinco linguagens

SDKEnvio com image/pngEnvio com text/htmlDownload
TypeScript@aws-sdk/client-s3 3.1169200403anexo, application/octet-stream
Pythonboto3 1.44200403anexo, application/octet-stream
Goaws-sdk-go-v2 (service/s3 1.114)200403anexo, application/octet-stream
JavaAWS SDK for Java 2.31200403anexo, application/octet-stream
C#AWSSDK.S3200403anexo, application/octet-stream

A versão TypeScript só chegou a essa linha depois de duas correções, que o teste exigiu:

Versão do código TypeScriptEnvio com image/pngEnvio com text/html
Sem opções extras400 BadDigest400
Com requestChecksumCalculation: "WHEN_REQUIRED"200200: o tipo não estava assinado
E com signableHeaders: new Set(["content-type"])200403

O que muda de uma linguagem para outra

Gerar a URLO tipo entra na assinatura?
TypeScriptgetSignedUrl com o comandoSó com signableHeaders
Pythongenerate_presigned_urlSim, ao informar ContentType
GoPresignClient.PresignPutObjectSim
JavaS3Presigner.presignPutObjectSim
C#GetPreSignedURLAsyncSim

Os comentários nos exemplos marcam o que só existe por causa do servidor de teste: o endereço próprio, o estilo de caminho (bucket na URL, e não no nome do domínio) e, no C#, o protocolo HTTP. Na AWS, essas linhas saem.

Limitar o tamanho com POST e política

Para fotos e documentos, a URL de envio por PUT não basta: no teste, ela aceitou 20 MB. O POST com política, testado em Python:

envio = s3.generate_presigned_post(
    "arquivos", "fotos/4.png",
    Fields={"Content-Type": "image/png"},
    Conditions=[{"Content-Type": "image/png"}, ["content-length-range", 1, 1_000_000]],
    ExpiresIn=300,
)
# O navegador envia um formulário para envio["url"], com os campos de envio["fields"] e o arquivo

Com essa política, um arquivo de 500 KB foi aceito (204) e um de 2 MB foi recusado com EntityTooLarge. No navegador, o envio é um FormData com os campos devolvidos e o arquivo por último.

O envio no navegador

Com a URL de PUT, o navegador envia o arquivo com o mesmo tipo da assinatura:

const { url } = await (await fetch("/api/uploads", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ tipo: arquivo.type, tamanho: arquivo.size }),
})).json();

const envio = await fetch(url, {
  method: "PUT",
  headers: { "content-type": arquivo.type }, // precisa ser igual ao tipo assinado
  body: arquivo,
});
if (!envio.ok) throw new Error(`envio falhou: ${envio.status}`);

Para o navegador enviar direto, o bucket precisa permitir pedidos da origem do site, com uma regra de CORS para PUT (ou POST). Este trecho não foi executado no teste, que enviou os arquivos com curl.

Lista de revisão

Armazenamento

  • Os arquivos ficam no armazenamento de objetos, e o banco guarda só os dados sobre eles?
  • O bucket é privado, com bloqueio de acesso público?
  • A aplicação usa um papel da nuvem com permissão só no necessário, em vez de chaves fixas?

Envio

  • O navegador envia direto, com URL assinada, ou a aplicação repassa em fluxo?
  • O tipo está na assinatura? (No SDK de JavaScript, com signableHeaders.)
  • Há limite de tamanho, com POST e política ou conferência depois do envio?
  • A chave é gerada pela aplicação, depois de conferir a autorização?
  • A aplicação confere o objeto antes de registrá-lo?

Entrega

  • Os downloads usam URLs assinadas de validade curta, ou uma CDN com credencial própria?
  • Arquivos de usuários saem como anexo, ou de um domínio separado?

Ciclo de vida

  • Há regras para mover arquivos antigos, apagar temporários e envios em partes abandonados?
  • Os arquivos importantes têm versionamento ou bloqueio contra exclusão?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Implementar o envio direto do navegador

    Quero implementar o envio de arquivos direto do navegador para o armazenamento.
    
    Pergunte qual linguagem e framework eu uso no servidor, qual armazenamento, e o que o front-end usa.
    
    Vamos em etapas:
    1. A rota que confere a autorização, escolhe a chave e gera a URL assinada com tipo e tamanho.
    2. O código do navegador que envia o arquivo.
    3. A rota que confirma o envio, conferindo o objeto antes de registrar.
    4. A regra de CORS do bucket.
    5. Testes: tipo errado, arquivo grande demais, URL vencida, chave trocada.
  • Processar arquivos depois do envio

    Depois que o usuário envia um arquivo, quero processá-lo (miniaturas, conferência do tipo real, antivírus).
    
    Pergunte qual armazenamento, linguagem e fila eu uso, e o que precisa ser feito com cada tipo de arquivo.
    
    Me ajude a montar:
    1. O aviso de objeto criado (eventos do bucket ou a confirmação da aplicação).
    2. A tarefa em segundo plano que processa, de forma idempotente.
    3. A conferência do tipo real pelo conteúdo.
    4. O estado do arquivo no banco (enviado, processado, recusado).
    5. O que fazer com arquivos recusados.
  • Revisar uma implementação de upload

    Vou colar abaixo o código de envio e entrega de arquivos de uma aplicação, e a configuração do bucket, se houver. Revise.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. Os arquivos ficam no armazenamento de objetos, e não no disco ou no banco?
    2. A aplicação recebe o arquivo inteiro na memória?
    3. A URL assinada fixa o tipo e limita o tamanho, e tem validade curta?
    4. A autorização é conferida antes de gerar a URL, e a chave é gerada pela aplicação?
    5. Os arquivos de usuários são entregues como anexo ou de outro domínio?
    6. O bucket é privado, e a aplicação usa permissões mínimas?
    
    Para cada problema, descreva como explorá-lo e mostre a correção.
    
    Código e configuração:
    [cole aqui]