Archive

Чтение и запись архивов zip/tar с ограниченным потреблением памяти. Архивы никогда не загружаются в RAM и не распаковываются на диск — пиковое потребление памяти не зависит от размера архива и записи, так что архивы в несколько ГБ обрабатываются на сервере с малым объёмом RAM.

Загрузка

local archive = require("archive")

Форматы

Встроенные форматы определяются по магическим байтам либо задаются принудительно через opts.format:

Формат Произвольное чтение Последовательный проход Запись
zip да да (локальные заголовки) да
tar да да да
tar.gz нет да да
tar.zst нет да да

archive.formats() возвращает список имён зарегистрированных форматов.

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

Опции

Все точки входа принимают необязательную таблицу opts:

Ключ По умолчанию Значение
format авто "zip", "tar", "tar.gz", "tar.zst"; авто = определение по магическим байтам, иначе по расширению
max_entries 100000 Отклонять архивы с большим числом записей (защита от бомб сжатия)
max_total_bytes 2 ГиБ Ограничение на суммарный несжатый вывод при чтении/распаковке
max_file_bytes 1 ГиБ Ограничение на несжатый размер одной записи
max_inline_bytes 16 МиБ Жёсткое ограничение для вызова read(), материализующего данные в RAM; выше него используйте stream()/extract()
buffer_bytes 64 КиБ Буфер потокового копирования для чтения/распаковки/добавления

max_total_bytes/max_file_bytes — это ограничения на объём работы, а не на RAM: потоковая обработка записи никогда не удерживает больше, чем buffer_bytes плюс окно распаковки кодека. Единственный параметр, задающий объём RAM, — max_inline_bytes.

Чтение — произвольный доступ

archive.open(source, ...) открывает позиционируемый источник для полного произвольного доступа (центральный каталог zip читается заранее; записи распаковываются по требованию). Источником может быть дескриптор fs.FS вместе с путём, открытый fs.File, сырые байты (байты держат весь архив в RAM — только для небольших архивов) или любой читатель с произвольным доступом, переданный другим модулем.

Читатель из другого модуля подходит, если он реализует io.ReaderAt и сообщает свой Size; необязательное Name используется для определения формата по расширению, когда opts.format не указан. Один из таких — open_reader модуля cloudstorage, который читает архив в несколько ГБ прямо из объектного хранилища. В этом случае архив ничего не открывает и никогда не закрывает читателя — это делает его владелец.

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

-- Открытие по дескриптору fs + пути (модуль открывает файл и владеет его жизненным циклом)
local r, err = archive.open(fs.get("app:uploads"), "incoming.zip")
-- Или из уже открытого позиционируемого fs.File
-- local r = archive.open(fs:get("app:uploads"):open("x.zip"))
-- Или из сырых байтов (только для небольших архивов)
-- local r = archive.open(zip_bytes, { format = "zip" })
-- Или из читателя с произвольным доступом, которым владеет другой модуль
-- local reader = cloudstorage.get("app:files"):open_reader("incoming.zip")
-- local r = archive.open(reader)

Возвращает: Reader, error

Разрешение: archive.read

entries

Обход каталога записей (только метаданные — без распаковки):

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

Получить метаданные записи по имени (без распаковки):

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

read

Материализовать одну запись как строку Lua. Выдаёт ошибку (kind = Invalid) при превышении max_inline_bytes — для чего-то крупного используйте stream() или extract():

local data, err = r:read("docs/readme.md")  -- только небольшие записи

stream

Вернуть запись как stream.Stream, распаковывающий данные по требованию. Компонуется везде, где работает поток — :scanner(), fs:writefile() или передача в другой модуль:

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

Потоково записать одну запись в целевую файловую систему:

local ok, err = r:extract("docs/readme.md", fs.get("app:out"))
-- необязательный путь назначения:
-- r:extract("docs/readme.md", fs.get("app:out"), "readme.md")

extract_all

Потоково записать все записи в целевую файловую систему:

local count, err = r:extract_all(fs.get("app:out"), {
    prefix = "job123/",          -- добавить к каждому пути назначения
    strip  = 1,                  -- отбросить N ведущих компонентов пути
    filter = function(e) return not e.is_dir end,
})

Имена записей санируются при распаковке — сегменты .., абсолютные пути и префиксы дисков/UNC Windows отклоняются (защита от zip-slip).

close

Закрыть читателя. Идемпотентно; также закрывается автоматически по завершении задачи.

r:close()

Чтение — последовательный проход

archive.scan(source, opts?) открывает поток только вперёд (тело HTTP-загрузки, поток файла из multipart). Записи посещаются в порядке следования в архиве; читатель каждой записи действителен только до перехода к следующей. Произвольного read(name) нет.

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 — это stream.Stream
    if not e.is_dir then
        fs.get("app:uploads"):writefile("job123/" .. e.name, entry)
    end
end
s:close()

Возвращает: Walker, error

Разрешение: archive.read

Walker также поддерживает extract_all с теми же опциями, что и читатель с произвольным доступом, потоково выгружая каждую запись в целевую файловую систему за один вызов:

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

tar, tar.gz и tar.zst обрабатываются потоково нативно. zip разбирается по локальным заголовкам записей; записи, записанные с потоковым дескриптором данных (размер/CRC следуют за данными), читаются распаковкой до границы записи. Для надёжной обработки zip при больших загрузках сначала сохраните загрузку как файл (ограниченное последовательное копирование), а затем используйте archive.open:

local dst = fs.get("app:tmp")
dst:writefile("u.zip", req:stream())   -- потоковое копирование загрузки → файл fs
local r = archive.open(dst, "u.zip")   -- надёжный произвольный доступ
-- ... entries / extract_all ...
r:close()
dst:remove("u.zip")

Запись

archive.create(dest, ...) строит архив, потоково записывая записи в назначение — файл в fs (с указанием пути) или записываемый stream.Stream (например, HTTP-ответ), так что скачиваемый .zip генерируется сразу в сеть с ограниченным потреблением памяти.

local w, err = archive.create(fs.get("app:tmp"), "out.zip", { format = "zip" })
-- или потоком в ответ:
-- local w = archive.create(res:stream(), { format = "zip" })

Возвращает: Writer, error

Разрешение: archive.write

add

Добавить запись из строки, байтов, читателя или stream.Stream:

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

add_file

Потоково добавить запись из файла в файловой системе:

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

add_dir

Добавить запись каталога:

w:add_dir("empty/")

close

Финализировать архив (для zip записывает центральный каталог). Идемпотентно; также закрывается автоматически по завершении задачи.

w:close()

Опции add*: { method = "store"|"deflate", mode, size }. Форматам tar нужен размер записи заранее, поэтому add() из потока или читателя в архив tar* требует size (строки и add_file предоставляют его сами). Писатель zip работает потоково с непозиционируемыми писателями, используя дескрипторы данных, поэтому запись в поток ответа работает.

Ошибки

Условие Вид
Источник не является дескриптором fs, файлом fs, байтами или читателем с произвольным доступом errors.INVALID
Неизвестный / несовпадающий формат errors.INVALID
Повреждённый или обрезанный архив errors.INVALID
Превышено ограничение (записи / общий объём / файл / inline) errors.INVALID
Произвольный доступ к формату, поддерживающему только поток (используйте scan) errors.UNAVAILABLE
Имя записи не найдено errors.NOT_FOUND
Источник недоступен для чтения / назначение недоступно для записи errors.PERMISSION_DENIED
Чтение устаревшей потоковой записи после перехода обхода дальше errors.INTERNAL

О работе с ошибками см. Обработка ошибок.

См. также

  • Filesystem - Исходная и целевая файловые системы
  • Stream - Объекты потоков, передаваемые в архивы и из них
  • Compression - Сжатие gzip/deflate/zstd в памяти
  • Cloud Storage - open_reader как источник архива с произвольным доступом