ionian/packages/server/src/util/zipwrap.ts

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;
}