Pular para conteúdo

upload: Leitor e parser de streaming para upload de arquivos http baseado no nginx-module-lua cosocket

Instalação

Se você ainda não configurou a assinatura do repositório RPM, inscreva-se. Depois, você pode prosseguir com os seguintes passos.

CentOS/RHEL 7 ou Amazon Linux 2

yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-upload

CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023

dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-upload

Para usar esta biblioteca Lua com o NGINX, certifique-se de que o nginx-module-lua está instalado.

Este documento descreve o lua-resty-upload v0.11 lançado em 19 de janeiro de 2023.


Esta biblioteca Lua é uma API de upload de arquivos em streaming para o módulo ngx_lua do nginx:

http://wiki.nginx.org/HttpLuaModule

O tipo MIME multipart/form-data é suportado.

A API desta biblioteca retorna tokens um por um. O usuário só precisa chamar o método read repetidamente até que um tipo de token nil seja retornado. Para cada token retornado do método read, basta verificar o primeiro valor de retorno para o tipo de token atual. O tipo de token pode ser header, body e part end. Cada campo de formulário multipart/form-data analisado consiste em vários tokens header contendo cada cabeçalho de campo, vários tokens body contendo cada pedaço de dados do corpo e uma flag part end indicando o fim do campo.

É assim que a leitura em streaming funciona. Mesmo para gigabytes de dados de arquivo de entrada, a memória usada no ambiente Lua pode ser pequena e constante, desde que o usuário não acumule os pedaços de dados de entrada por conta própria.

Esta biblioteca Lua aproveita a API de cosocket do ngx_lua, que garante um comportamento 100% não bloqueante.

Note que pelo menos ngx_lua 0.7.9 ou OpenResty 1.2.4.14 é necessário.

Sinopse

    server {
        location /test {
            content_by_lua '
                local upload = require "resty.upload"
                local cjson = require "cjson"

                local chunk_size = 5 -- deve ser definido como 4096 ou 8192
                                     -- para configurações do mundo real

                local form, err = upload:new(chunk_size)
                if not form then
                    ngx.log(ngx.ERR, "falha ao criar upload: ", err)
                    ngx.exit(500)
                end

                form:set_timeout(1000) -- 1 seg

                while true do
                    local typ, res, err = form:read()
                    if not typ then
                        ngx.say("falha ao ler: ", err)
                        return
                    end

                    ngx.say("lido: ", cjson.encode({typ, res}))

                    if typ == "eof" then
                        break
                    end
                end

                local typ, res, err = form:read()
                ngx.say("lido: ", cjson.encode({typ, res}))
            ';
        }
    }

Uma saída típica da localização /test definida acima é:

lido: ["header",["Content-Disposition","form-data; name=\"file1\"; filename=\"a.txt\"","Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\""]]
lido: ["header",["Content-Type","text\/plain","Content-Type: text\/plain"]]
lido: ["body","Hello"]
lido: ["body",", wor"]
lido: ["body","ld"]
lido: ["part_end"]
lido: ["header",["Content-Disposition","form-data; name=\"test\"","Content-Disposition: form-data; name=\"test\""]]
lido: ["body","value"]
lido: ["body","\r\n"]
lido: ["part_end"]
lido: ["eof"]
lido: ["eof"]

Você pode usar a biblioteca lua-resty-string para calcular o digest SHA-1 e MD5 dos dados do arquivo de forma incremental. Aqui está um exemplo:

    local resty_sha1 = require "resty.sha1"
    local upload = require "resty.upload"

    local chunk_size = 4096
    local form = upload:new(chunk_size)
    local sha1 = resty_sha1:new()
    local file
    while true do
        local typ, res, err = form:read()

        if not typ then
             ngx.say("falha ao ler: ", err)
             return
        end

        if typ == "header" then
            local file_name = my_get_file_name(res)
            if file_name then
                file = io.open(file_name, "w+")
                if not file then
                    ngx.say("falha ao abrir arquivo ", file_name)
                    return
                end
            end

         elseif typ == "body" then
            if file then
                file:write(res)
                sha1:update(res)
            end

        elseif typ == "part_end" then
            file:close()
            file = nil
            local sha1_sum = sha1:final()
            sha1:reset()
            my_save_sha1_sum(sha1_sum)

        elseif typ == "eof" then
            break

        else
            -- não faz nada
        end
    end

Se você quiser calcular somas MD5 para os arquivos enviados, basta usar o módulo resty.md5 fornecido pela biblioteca lua-resty-string. Ele possui uma API semelhante à do resty.sha1.

Para upload de arquivos grandes, é importante não armazenar todos os dados na memória. Ou seja, você nunca deve acumular pedaços de dados nem em uma grande string Lua nem em uma grande tabela Lua. Você deve escrever o pedaço de dados em arquivos o mais rápido possível e descartar o pedaço de dados imediatamente (para permitir que o GC do Lua o libere).

Em vez de escrever o pedaço de dados em arquivos (como mostrado no exemplo acima), você também pode escrever os pedaços de dados em conexões de cosocket upstream se não quiser salvar os dados em sistemas de arquivos locais.

Uso

local upload = require "resty.upload"
local form, err = upload:new(self, chunk_size, max_line_size, preserve_body)
chunk_size padrão é 4096. É o tamanho usado para ler dados do socket.

max_line_size padrão é 512. É o limite de tamanho para ler o cabeçalho do corpo em pedaços.

Por padrão, lua-resty-upload consumirá o corpo da requisição. Para o modo proxy, isso significa que o upstream não verá o corpo. Quando preserve_body é definido como true, o corpo da requisição será preservado. Note que esta opção não é gratuita. Quando ativada, dobrará o uso de memória do resty.upload.

Veja Também

GitHub

Você pode encontrar dicas adicionais de configuração e documentação para este módulo no repositório GitHub para nginx-module-upload.