Receber arquivos
multipart/form-data, e as três recusas que separam um upload de uma porta aberta.
O corpo de um pedido é interpretado pelo Content-Type. Com multipart/form-data, os campos comuns vão para req["body"] e os arquivos para req["files"].
adopt Kiln
server importador on 8080:
route POST "/importar":
arquivo := Kiln.upload(req, "planilha")
given arquivo is void:
respond 400 json {"erro": "nenhum arquivo em 'planilha'"}
linhas := arquivo["texto"].lines() >> sift l: l.trim() is not ""
gravado := Kiln.salvar_upload(arquivo, "envios",
tipos := [".csv", ".xlsx"],
limite := 5242880)
respond json {
"titulo": req["body"]["titulo"] ?? "",
"linhas": len(linhas),
"em": gravado["caminho"]
}O que chega#
| Chamada | Devolve |
|---|---|
Kiln.upload(req, campo) | o vault de um arquivo, ou void |
Kiln.uploads(req) | todos, por nome de campo |
req["body"] | os campos comuns, como num formulário qualquer |
req["files"] | o mesmo que Kiln.uploads(req) |
{
"nome": "vendas.csv", // já sem o caminho do cliente
"tipo": "text/csv",
"tamanho": 1284,
"conteudo": <bytes>,
"texto": "id,valor\n1,10\n…"
}Campos e arquivos ficam separados de propósito: quem escreve lê body["titulo"] sem saber se o formulário tinha arquivo, e um cycle sobre body não topa com bytes onde espera texto.
O caminho que o cliente manda é descartado: o IE mandava C:\Users\ana\foto.jpg, e um navegador hostil manda o que quiser. Fica só foto.jpg.
Gravar#
gravado := Kiln.salvar_upload(arquivo, "envios",
tipos := [".csv", ".xlsx"],
limite := 5242880)
// {"caminho": "envios/a3f91c2e4b08.csv",
// "nome": "a3f91c2e4b08.csv",
// "nome_original": "vendas.csv",
// "tamanho": 1284}| Recusa | Por quê |
|---|---|
nome com /, \ ou .. | ../../.ssh/authorized_keys escreve fora da pasta de destino |
acima do limite | um upload de 4 GB enche o disco |
extensão fora de tipos | .php numa pasta servida como estática é execução remota |
E o nome final nunca é o que o cliente mandou: leva um prefixo aleatório. Dois usuários enviando foto.jpg não podem sobrescrever um ao outro, e um nome escolhido por quem envia é um nome que ele pode adivinhar depois. nome_original volta no resultado, para guardar no banco e mostrar ao usuário.
Dois limites, e eles são diferentes#
Kiln.config(app, "limite_corpo", 20971520) // 20 MB — o do PEDIDO
Kiln.salvar_upload(arquivo, pasta, limite := 5242880) // 5 MB — o do ARQUIVOO limite do pedido é verificado antes de o corpo ser lido na memória: um POST maior é recusado com 413 sem chegar à rota. Sem ele, um POST de 2 GB derruba o processo sem exploit nenhum. O padrão é 10 MB.
O do arquivo é por arquivo, depois de o corpo estar lido. Aumentar um sem o outro não funciona.
Vários arquivos#
route POST "/galeria":
arquivos := Kiln.uploads(req)
salvos := []
cycle nome, arquivo in arquivos:
salvos.append(Kiln.salvar_upload(arquivo, "fotos",
tipos := [".jpg", ".png", ".webp"]))
respond json {"salvos": len(salvos)}Testar sem navegador#
Kiln.test aceita o corpo cru — basta montar o multipart, que é o que o exercício 223 faz:
corpo := "--X\r\n" +
"Content-Disposition: form-data; name=\"titulo\"\r\n\r\n" +
"Vendas\r\n" +
"--X\r\n" +
"Content-Disposition: form-data; name=\"planilha\"; " +
"filename=\"v.csv\"\r\nContent-Type: text/csv\r\n\r\n" +
"id,valor\n1,10\n\r\n" +
"--X--\r\n"
r := Kiln.test(app, "POST", "/importar", corpo,
{"content-type": "multipart/form-data; boundary=X"})
assert r["status"] is 200