108 lines
2.7 KiB
TypeScript
108 lines
2.7 KiB
TypeScript
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<string> {
|
|
private fd?: FileHandle;
|
|
private path: string;
|
|
private closed = false;
|
|
|
|
constructor(path: string) {
|
|
super(path);
|
|
this.path = path;
|
|
}
|
|
|
|
async init(): Promise<void> {
|
|
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<void> {
|
|
if (this.closed) return;
|
|
this.closed = true;
|
|
|
|
if (this.fd) {
|
|
await this.fd.close();
|
|
this.fd = undefined;
|
|
}
|
|
}
|
|
|
|
async readUint8Array(index: number, length: number): Promise<Uint8Array> {
|
|
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<FileHandle> {
|
|
private closed = false;
|
|
|
|
constructor(private reader: FileReader, options?: ZipReaderConstructorOptions) {
|
|
super(reader, options);
|
|
}
|
|
|
|
override async close(): Promise<void> {
|
|
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<FileHandle>
|
|
}> {
|
|
const reader = new FileZipReader(new FileReader(path), {
|
|
useCompressionStream: true,
|
|
preventClose: false,
|
|
});
|
|
return { reader };
|
|
}
|
|
export async function entriesByNaturalOrder(zip: ZipReader<FileHandle>) {
|
|
const entries = await zip.getEntries();
|
|
const ret = orderBy(entries, (v) => v.filename);
|
|
return ret;
|
|
}
|
|
|
|
export async function createReadableStreamFromZip(_zip: ZipReader<FileHandle>, entry: Entry): Promise<ReadableStream> {
|
|
if (entry.getData === undefined) {
|
|
throw new Error("entry.getData is undefined");
|
|
}
|
|
const stream = new TransformStream<Uint8Array, Uint8Array>();
|
|
entry.getData(stream.writable);
|
|
return stream.readable;
|
|
}
|