Archive

제한된 메모리로 zip/tar 아카이브를 읽고 씁니다. 아카이브는 RAM으로 로드되지도, 디스크로 추출되지도 않습니다. 최대 메모리는 아카이브와 항목 크기와 무관하므로, 수 GB 아카이브도 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 GiB 읽기/추출 중 누적 비압축 출력 상한
max_file_bytes 1 GiB 단일 항목의 비압축 크기 상한
max_inline_bytes 16 MiB RAM에 실체화하는 read() 호출의 절대 상한, 그 이상은 stream()/extract() 사용
buffer_bytes 64 KiB 읽기/추출/추가를 위한 스트리밍 복사 버퍼

max_total_bytes/max_file_bytes는 작업량 상한이지 RAM 상한이 아닙니다. 항목을 스트리밍할 때는 buffer_bytes에 코덱의 압축 해제 윈도를 더한 것 이상을 절대 보유하지 않습니다. RAM 크기를 조정하는 유일한 손잡이는 max_inline_bytes입니다.

읽기 — 랜덤 액세스

archive.open(source, ...)은 완전한 랜덤 액세스를 위해 탐색 가능한(seekable) 소스를 엽니다(zip 중앙 디렉터리는 미리 읽고, 항목은 필요할 때 압축을 해제합니다). 소스는 fs.FS 핸들과 경로, 열린 fs.File, 원시 바이트(바이트는 아카이브 전체를 RAM에 보유하므로 작은 아카이브만), 또는 다른 모듈이 넘겨준 임의의 랜덤 액세스 리더일 수 있습니다.

다른 모듈의 리더는 io.ReaderAt을 구현하고 Size를 보고할 때 자격을 갖춥니다. 선택적 Name은 opts.format이 생략되었을 때 확장자 감지에 사용됩니다. cloudstorage의 open_reader가 그런 예로, 수 GB 아카이브를 오브젝트 스토리지에서 직접 읽습니다. 이 경우 아카이브는 아무것도 열지 않고 리더를 절대 닫지 않습니다. 리더의 소유자가 닫습니다.

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 문자열로 실체화합니다. max_inline_bytes를 초과하면 오류(kind = Invalid)가 발생합니다. 큰 것에는 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,
})

항목 이름은 추출 시 정리됩니다. .. 세그먼트, 절대 경로, Windows 드라이브/UNC 접두사는 거부됩니다(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

워커도 랜덤 액세스 리더와 동일한 옵션을 받는 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 포맷은 항목 크기를 미리 알아야 하므로, 스트림이나 리더에서 tar* 아카이브로 add()할 때는 size가 필요합니다(문자열과 add_file은 크기를 스스로 제공합니다). zip 라이터는 데이터 디스크립터를 사용해 탐색 불가능한 라이터로도 스트리밍하므로, 응답 스트림에 쓰는 것도 동작합니다.

오류

조건 종류
소스가 fs 핸들, fs 파일, 바이트, 랜덤 액세스 리더 중 어느 것도 아님 errors.INVALID
알 수 없거나 일치하지 않는 포맷 errors.INVALID
손상되었거나 잘린 아카이브 errors.INVALID
한도 초과 (항목 수 / 전체 / 파일 / 인라인) errors.INVALID
스트림 전용 포맷에 대한 랜덤 액세스 (scan 사용) errors.UNAVAILABLE
항목 이름을 찾을 수 없음 errors.NOT_FOUND
소스를 읽을 수 없거나 대상에 쓸 수 없음 errors.PERMISSION_DENIED
순회가 진행된 뒤 오래된 스트리밍 항목을 읽음 errors.INTERNAL

오류를 다루는 방법은 오류 처리를 참조하십시오.

참고