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как источник архива с произвольным доступом