Pular para o conteúdo

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"].

dataforge
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#

ChamadaDevolve
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)
text
{
    "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#

dataforge
gravado := Kiln.salvar_upload(arquivo, "envios",
                              tipos := [".csv", ".xlsx"],
                              limite := 5242880)
// {"caminho": "envios/a3f91c2e4b08.csv",
//  "nome": "a3f91c2e4b08.csv",
//  "nome_original": "vendas.csv",
//  "tamanho": 1284}
RecusaPor quê
nome com /, \ ou ..../../.ssh/authorized_keys escreve fora da pasta de destino
acima do limiteum 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#

dataforge
Kiln.config(app, "limite_corpo", 20971520)     // 20 MB  o do PEDIDO
Kiln.salvar_upload(arquivo, pasta, limite := 5242880)  // 5 MB  o do ARQUIVO

O 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#

dataforge
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:

dataforge
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

Onde continuar#