Archive

Leia e escreva arquivos zip/tar com memória limitada. Os arquivos nunca são carregados em RAM nem extraídos para disco — o pico de memória independe do tamanho do arquivo e das entradas, então arquivos de vários GB rodam em um servidor com pouca RAM.

Carregamento

local archive = require("archive")

Formatos

Os formatos integrados são detectados por magic bytes, ou forçados com opts.format:

Formato Leitura aleatória Varredura sequencial Escrita
zip sim sim (cabeçalhos locais) sim
tar sim sim sim
tar.gz não sim sim
tar.zst não sim sim

archive.formats() retorna a lista de nomes de formatos registrados.

local names = archive.formats()  -- {"zip", "tar", "tar.gz", "tar.zst", ...}

Opções

Todos os pontos de entrada aceitam uma tabela opts opcional:

Chave Padrão Significado
format auto "zip", "tar", "tar.gz", "tar.zst"; auto = detecta magic bytes, senão a extensão
max_entries 100000 Rejeita arquivos com mais entradas (defesa contra bomba de descompressão)
max_total_bytes 2 GiB Limite da saída descomprimida acumulada durante leitura/extração
max_file_bytes 1 GiB Limite do tamanho descomprimido de uma única entrada
max_inline_bytes 16 MiB Limite rígido para a chamada read(), que materializa em RAM; acima dele, use stream()/extract()
buffer_bytes 64 KiB Buffer de cópia por streaming para leitura/extração/adição

max_total_bytes/max_file_bytes são limites de trabalho, não limites de RAM — fazer streaming de uma entrada nunca retém mais que buffer_bytes mais a janela de descompressão do codec. O único ajuste que dimensiona a RAM é max_inline_bytes.

Leitura — Acesso Aleatório

archive.open(source, ...) abre uma fonte seekable para acesso aleatório completo (o diretório central do zip é lido antecipadamente; as entradas são descomprimidas sob demanda). A fonte pode ser um handle fs.FS mais um caminho, um fs.File aberto, bytes brutos (bytes mantêm o arquivo inteiro em RAM — apenas arquivos pequenos), ou qualquer leitor de acesso aleatório entregue por outro módulo.

Um leitor de outro módulo se qualifica quando implementa io.ReaderAt e informa seu Size; um Name opcional é usado para detecção pela extensão quando opts.format é omitido. O open_reader de cloudstorage é um deles, e lê um arquivo de vários GB diretamente do armazenamento de objetos. Nesse caso o archive não abre nada e nunca fecha o leitor — quem o fecha é seu dono.

local fs = require("fs")
local archive = require("archive")

-- Abre por handle de fs + caminho (o módulo abre o arquivo e é dono do seu ciclo de vida)
local r, err = archive.open(fs.get("app:uploads"), "incoming.zip")
-- Ou a partir de um fs.File seekable já aberto
-- local r = archive.open(fs:get("app:uploads"):open("x.zip"))
-- Ou a partir de bytes brutos (apenas arquivos pequenos)
-- local r = archive.open(zip_bytes, { format = "zip" })
-- Ou a partir de um leitor de acesso aleatório pertencente a outro módulo
-- local reader = cloudstorage.get("app:files"):open_reader("incoming.zip")
-- local r = archive.open(reader)

Retorna: Reader, error

Permissão: archive.read

entries

Itera o diretório (apenas metadados — sem descompressão):

for e in r:entries() do
    -- e: name, size, compressed_size, is_dir, mode, modified, method, crc32, type
    print(e.name, e.size, e.is_dir)
end

stat

Obtém os metadados de uma entrada pelo nome (sem descompressão):

local info, err = r:stat("docs/readme.md")

read

Materializa uma única entrada como uma string Lua. Falha (kind = Invalid) acima de max_inline_bytes — para qualquer coisa grande, use stream() ou extract():

local data, err = r:read("docs/readme.md")  -- apenas entradas pequenas

stream

Retorna a entrada como um stream.Stream que descomprime sob demanda. Compõe-se em todo lugar em que um stream se compõe — :scanner(), fs:writefile(), ou entregue a outro módulo:

local es, err = r:stream("big.csv")
while true do
    local chunk = es:read(65536)
    if not chunk then break end
    process(chunk)
end
es:close()

extract

Faz streaming de uma entrada para um sistema de arquivos de destino:

local ok, err = r:extract("docs/readme.md", fs.get("app:out"))
-- caminho de destino opcional:
-- r:extract("docs/readme.md", fs.get("app:out"), "readme.md")

extract_all

Faz streaming de cada entrada para um sistema de arquivos de destino:

local count, err = r:extract_all(fs.get("app:out"), {
    prefix = "job123/",          -- prefixa cada caminho de destino
    strip  = 1,                  -- descarta N componentes iniciais do caminho
    filter = function(e) return not e.is_dir end,
})

Os nomes das entradas são sanitizados na extração — segmentos .., caminhos absolutos e prefixos de unidade/UNC do Windows são rejeitados (defesa contra zip-slip).

close

Fecha o leitor. Idempotente; também fechado automaticamente no escopo da task.

r:close()

Leitura — Varredura Sequencial

archive.scan(source, opts?) abre um stream somente para frente (um corpo de upload HTTP, um stream de arquivo multipart). As entradas são visitadas na ordem do arquivo; o leitor de cada entrada é válido apenas até você avançar. Não há read(name) aleatório.

local up = form.files.upload[1]:stream()        -- stream.Stream
local s, err = archive.scan(up, { format = "zip" })

for e, entry in s:walk() do                      -- entry é um stream.Stream
    if not e.is_dir then
        fs.get("app:uploads"):writefile("job123/" .. e.name, entry)
    end
end
s:close()

Retorna: Walker, error

Permissão: archive.read

Um walker também suporta extract_all com as mesmas opções do leitor de acesso aleatório, transmitindo cada entrada para um filesystem de destino em uma única chamada:

local count, err = s:extract_all(fs.get("app:uploads"), { prefix = "job123/" })

tar, tar.gz e tar.zst fazem streaming nativamente. zip é analisado via cabeçalhos locais por entrada; entradas escritas com um data descriptor de streaming (tamanho/CRC após os dados) são lidas descomprimindo até o limite da entrada. Para um tratamento robusto de zip em uploads grandes, grave o upload como arquivo primeiro (uma cópia sequencial limitada) e então use archive.open:

local dst = fs.get("app:tmp")
dst:writefile("u.zip", req:stream())   -- cópia por streaming do upload → arquivo no fs
local r = archive.open(dst, "u.zip")   -- acesso aleatório robusto
-- ... entries / extract_all ...
r:close()
dst:remove("u.zip")

Escrita

archive.create(dest, ...) constrói um arquivo fazendo streaming das entradas para um destino — um arquivo em um fs (com um caminho) ou um stream.Stream gravável (por exemplo, uma resposta HTTP), de modo que um .zip de download é gerado direto para a rede com memória limitada.

local w, err = archive.create(fs.get("app:tmp"), "out.zip", { format = "zip" })
-- ou faça streaming para uma resposta:
-- local w = archive.create(res:stream(), { format = "zip" })

Retorna: Writer, error

Permissão: archive.write

add

Adiciona uma entrada a partir de uma string, bytes, leitor ou stream.Stream:

w:add("notes.txt", "hello")
w:add("from_upload", some_stream, { method = "deflate", mode = tonumber("644", 8) })

add_file

Faz streaming de uma entrada a partir de um arquivo em um sistema de arquivos:

w:add_file("data/big.bin", fs.get("app:data"), "big.bin")

add_dir

Adiciona uma entrada de diretório:

w:add_dir("empty/")

close

Finaliza o arquivo (escreve o diretório central no caso do zip). Idempotente; também fechado automaticamente no escopo da task.

w:close()

Opções de add*: { method = "store"|"deflate", mode, size }. Formatos tar precisam do tamanho da entrada antecipadamente, então add() a partir de um stream ou reader para um arquivo tar* exige size (strings e add_file o fornecem). O escritor de zip faz streaming para escritores não-seekable usando data descriptors, então escrever em um stream de resposta funciona.

Erros

Condição Tipo
A fonte não é um handle de fs, um arquivo de fs, bytes ou um leitor de acesso aleatório errors.INVALID
Formato desconhecido / incompatível errors.INVALID
Arquivo corrompido ou truncado errors.INVALID
Limite excedido (entradas / total / arquivo / inline) errors.INVALID
Acesso aleatório em um formato somente de stream (use scan) errors.UNAVAILABLE
Nome de entrada não encontrado errors.NOT_FOUND
Fonte não legível / destino não gravável errors.PERMISSION_DENIED
Leitura de uma entrada de stream obsoleta depois que a varredura avançou errors.INTERNAL

Veja Error Handling para trabalhar com erros.

Veja Também

  • Filesystem - Sistemas de arquivos de origem e destino
  • Stream - Objetos de stream entregues a e recebidos de arquivos
  • Compressão - gzip/deflate/zstd em memória
  • Cloud Storage - open_reader como fonte de arquivo com acesso aleatório