import { type FileHandle, open } from "node:fs/promises"; import { orderBy } from "natural-orderby"; import { ZipReader, Reader, type Entry, ZipReaderConstructorOptions } from "@zip.js/zip.js"; class FileReader extends Reader { private fd?: FileHandle; private path: string; private closed = false; constructor(path: string) { super(path); this.path = path; } async init(): Promise { await super.init?.(); if (this.closed) return; const fd = await open(this.path, "r"); const stat = await fd.stat(); this.fd = fd; this.size = stat.size; } async close(): Promise { if (this.closed) return; this.closed = true; if (this.fd) { await this.fd.close(); this.fd = undefined; } } async readUint8Array(index: number, length: number): Promise { if (this.closed) { throw new Error("FileReader is closed"); } try { const buffer = new Uint8Array(length); if (this.fd === undefined) { console.error("file handle is undefined", this.path); // reopen the file handle this.fd = await open(this.path, "r"); const stat = await this.fd.stat(); if (stat.size !== this.size) { console.error("file size changed", this.path, stat.size, this.size); throw new Error("file size changed"); } } const buf = await this.fd.read(buffer, 0, length, index); if (buf.bytesRead !== length) { console.error(`read error: ${buf.bytesRead} !== ${length}`); throw new Error("read error"); } return buffer; } catch (error) { console.error("read error", error); // 에러 발생 시 파일 핸들 정리 await this.close(); throw error; } } } class FileZipReader extends ZipReader { private closed = false; constructor(private reader: FileReader, options?: ZipReaderConstructorOptions) { super(reader, options); } override async close(): Promise { if (this.closed) return; this.closed = true; try { await super.close(); } finally { await this.reader.close(); } } } export async function readZip(path: string): Promise<{ reader: ZipReader }> { const reader = new FileZipReader(new FileReader(path), { useCompressionStream: true, preventClose: false, }); return { reader }; } export async function entriesByNaturalOrder(zip: ZipReader) { const entries = await zip.getEntries(); const ret = orderBy(entries, (v) => v.filename); return ret; } export async function createReadableStreamFromZip(_zip: ZipReader, entry: Entry): Promise { if (entry.getData === undefined) { throw new Error("entry.getData is undefined"); } const stream = new TransformStream(); entry.getData(stream.writable); return stream.readable; }