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"));import os
import boto3
from botocore.config import Config
# Na AWS, endpoint_url e o estilo de endereço saem; o SDK lê as credenciais do ambiente
s3 = boto3.client(
"s3",
endpoint_url=os.environ.get("S3_ENDPOINT"),
region_name="us-east-1",
config=Config(signature_version="s3v4", s3={"addressing_style": "path"}),
)
BUCKET = "arquivos"
def url_de_envio(chave: str, tipo: str, segundos: int = 300) -> str:
"""O navegador envia direto para o armazenamento. O tipo entra na assinatura."""
return s3.generate_presigned_url(
"put_object",
Params={"Bucket": BUCKET, "Key": chave, "ContentType": tipo},
ExpiresIn=segundos,
)
def url_de_download(chave: str, segundos: int = 300) -> str:
"""O download sai como anexo, e não como página do seu domínio."""
return s3.generate_presigned_url(
"get_object",
Params={
"Bucket": BUCKET,
"Key": chave,
"ResponseContentDisposition": "attachment",
"ResponseContentType": "application/octet-stream",
},
ExpiresIn=segundos,
)
print(url_de_envio("fotos/py.png", "image/png"))
print(url_de_download("fotos/py.png"))package main
import (
"context"
"fmt"
"os"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
const bucket = "arquivos"
// urlDeEnvio: o navegador envia direto para o armazenamento; o tipo entra na assinatura
func urlDeEnvio(
ctx context.Context, p *s3.PresignClient, chave, tipo string, validade time.Duration,
) (string, error) {
r, err := p.PresignPutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket), Key: aws.String(chave), ContentType: aws.String(tipo),
}, s3.WithPresignExpires(validade))
if err != nil {
return "", err
}
return r.URL, nil
}
// urlDeDownload: o download sai como anexo, e não como página do seu domínio
func urlDeDownload(
ctx context.Context, p *s3.PresignClient, chave string, validade time.Duration,
) (string, error) {
r, err := p.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucket), Key: aws.String(chave),
ResponseContentDisposition: aws.String("attachment"),
ResponseContentType: aws.String("application/octet-stream"),
}, s3.WithPresignExpires(validade))
if err != nil {
return "", err
}
return r.URL, nil
}
func main() {
ctx := context.Background()
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("us-east-1"))
if err != nil {
panic(err)
}
// Na AWS, BaseEndpoint e UsePathStyle saem
cliente := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String(os.Getenv("S3_ENDPOINT"))
o.UsePathStyle = true
})
p := s3.NewPresignClient(cliente)
envio, _ := urlDeEnvio(ctx, p, "fotos/go.png", "image/png", 5*time.Minute)
download, _ := urlDeDownload(ctx, p, "fotos/go.png", 5*time.Minute)
fmt.Println(envio)
fmt.Println(download)
}import java.net.URI;
import java.time.Duration;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
public class Urls {
static final String BUCKET = "arquivos";
// Na AWS, endpointOverride e pathStyleAccess saem; o SDK lê as credenciais do ambiente
static final S3Presigner PRESIGNER = S3Presigner.builder()
.region(Region.US_EAST_1)
.endpointOverride(URI.create(System.getenv("S3_ENDPOINT")))
.serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(true).build())
.build();
// O navegador envia direto para o armazenamento; o tipo entra na assinatura
static String urlDeEnvio(String chave, String tipo, Duration validade) {
var pedido = PutObjectRequest.builder().bucket(BUCKET).key(chave).contentType(tipo).build();
return PRESIGNER.presignPutObject(PutObjectPresignRequest.builder()
.signatureDuration(validade).putObjectRequest(pedido).build()).url().toString();
}
// O download sai como anexo, e não como página do seu domínio
static String urlDeDownload(String chave, Duration validade) {
var pedido = GetObjectRequest.builder().bucket(BUCKET).key(chave)
.responseContentDisposition("attachment")
.responseContentType("application/octet-stream").build();
return PRESIGNER.presignGetObject(GetObjectPresignRequest.builder()
.signatureDuration(validade).getObjectRequest(pedido).build()).url().toString();
}
public static void main(String[] args) {
System.out.println(urlDeEnvio("fotos/java.png", "image/png", Duration.ofMinutes(5)));
System.out.println(urlDeDownload("fotos/java.png", Duration.ofMinutes(5)));
}
}using Amazon.S3;
using Amazon.S3.Model;
// Na AWS, ServiceURL e ForcePathStyle saem; o SDK lê as credenciais do ambiente
var s3 = new AmazonS3Client(new AmazonS3Config
{
ServiceURL = Environment.GetEnvironmentVariable("S3_ENDPOINT"),
ForcePathStyle = true,
AuthenticationRegion = "us-east-1",
});
const string Bucket = "arquivos";
// O navegador envia direto para o armazenamento; o tipo entra na assinatura
Task<string> UrlDeEnvio(string chave, string tipo, TimeSpan validade) =>
s3.GetPreSignedURLAsync(new GetPreSignedUrlRequest
{
BucketName = Bucket, Key = chave, Verb = HttpVerb.PUT, ContentType = tipo,
Expires = DateTime.UtcNow.Add(validade),
Protocol = Protocol.HTTP, // só no teste, com o servidor local sem HTTPS
});
// O download sai como anexo, e não como página do seu domínio
Task<string> UrlDeDownload(string chave, TimeSpan validade)
{
var pedido = new GetPreSignedUrlRequest
{
BucketName = Bucket, Key = chave, Verb = HttpVerb.GET,
Expires = DateTime.UtcNow.Add(validade),
Protocol = Protocol.HTTP,
};
pedido.ResponseHeaderOverrides.ContentDisposition = "attachment";
pedido.ResponseHeaderOverrides.ContentType = "application/octet-stream";
return s3.GetPreSignedURLAsync(pedido);
}
Console.WriteLine(await UrlDeEnvio("fotos/cs.png", "image/png", TimeSpan.FromMinutes(5)));
Console.WriteLine(await UrlDeDownload("fotos/cs.png", TimeSpan.FromMinutes(5)));O resultado, nas cinco linguagens
| SDK | Envio com image/png | Envio com text/html | Download | |
|---|---|---|---|---|
| TypeScript | @aws-sdk/client-s3 3.1169 | 200 | 403 | anexo, application/octet-stream |
| Python | boto3 1.44 | 200 | 403 | anexo, application/octet-stream |
| Go | aws-sdk-go-v2 (service/s3 1.114) | 200 | 403 | anexo, application/octet-stream |
| Java | AWS SDK for Java 2.31 | 200 | 403 | anexo, application/octet-stream |
| C# | AWSSDK.S3 | 200 | 403 | anexo, 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 TypeScript | Envio com image/png | Envio com text/html |
|---|---|---|
| Sem opções extras | 400 BadDigest | 400 |
Com requestChecksumCalculation: "WHEN_REQUIRED" | 200 | 200: o tipo não estava assinado |
E com signableHeaders: new Set(["content-type"]) | 200 | 403 |
O que muda de uma linguagem para outra
| Gerar a URL | O tipo entra na assinatura? | |
|---|---|---|
| TypeScript | getSignedUrl com o comando | Só com signableHeaders |
| Python | generate_presigned_url | Sim, ao informar ContentType |
| Go | PresignClient.PresignPutObject | Sim |
| Java | S3Presigner.presignPutObject | Sim |
| C# | GetPreSignedURLAsync | Sim |
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 arquivoCom 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]