From 26b55be260d1725c7bde6a1fa43e3699c0b43243 Mon Sep 17 00:00:00 2001 From: monoid Date: Wed, 1 Oct 2025 00:15:20 +0900 Subject: [PATCH] fix: ensure proper closure of zip reader and file handles --- packages/server/src/route/comic.ts | 24 +++++++++++++++---- packages/server/src/util/zipwrap.ts | 37 ++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/packages/server/src/route/comic.ts b/packages/server/src/route/comic.ts index d10748b..30663e5 100644 --- a/packages/server/src/route/comic.ts +++ b/packages/server/src/route/comic.ts @@ -29,7 +29,7 @@ export async function renderComicPage({ path, page, reqHeaders, set }: RenderOpt if (page < 0 || page >= entries.length) { set.status = 404; - zip.reader.close(); + await zip.reader.close(); return null; } @@ -45,7 +45,7 @@ export async function renderComicPage({ path, page, reqHeaders, set }: RenderOpt const cachedDate = new Date(ifModifiedSince); if (!Number.isNaN(cachedDate.valueOf()) && lastModified <= cachedDate) { set.status = 304; - zip.reader.close(); + await zip.reader.close(); return null; } } @@ -57,6 +57,14 @@ export async function renderComicPage({ path, page, reqHeaders, set }: RenderOpt }, }); + let zipClosed = false; + const closeZip = async () => { + if (!zipClosed) { + zipClosed = true; + await zip.reader.close(); + } + }; + readStream.pipeTo(new WritableStream({ write(chunk) { nodeReadable.push(chunk); @@ -64,15 +72,21 @@ export async function renderComicPage({ path, page, reqHeaders, set }: RenderOpt close() { nodeReadable.push(null); }, + abort(err) { + nodeReadable.destroy(err); + }, })).catch((err) => { nodeReadable.destroy(err); }); nodeReadable.on("close", () => { - zip.reader.close(); + closeZip().catch(console.error); }); nodeReadable.on("error", () => { - zip.reader.close(); + closeZip().catch(console.error); + }); + nodeReadable.on("end", () => { + closeZip().catch(console.error); }); const ext = entry.filename.split(".").pop()?.toLowerCase() ?? "jpeg"; @@ -84,7 +98,7 @@ export async function renderComicPage({ path, page, reqHeaders, set }: RenderOpt set.status = 200; return nodeReadable; } catch (error) { - zip.reader.close(); + await zip.reader.close(); throw error; } } diff --git a/packages/server/src/util/zipwrap.ts b/packages/server/src/util/zipwrap.ts index 6589e8e..dc7cf7c 100644 --- a/packages/server/src/util/zipwrap.ts +++ b/packages/server/src/util/zipwrap.ts @@ -1,11 +1,11 @@ import { type FileHandle, open } from "node:fs/promises"; import { orderBy } from "natural-orderby"; import { ZipReader, Reader, type Entry, ZipReaderConstructorOptions } from "@zip.js/zip.js"; -import EventEmitter from "node:events"; class FileReader extends Reader { private fd?: FileHandle; private path: string; + private closed = false; constructor(path: string) { super(path); @@ -14,21 +14,29 @@ class FileReader extends Reader { 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; - // not implemented yet - (this.fd as unknown as EventEmitter).on("close", () => { - this.fd?.close(); - this.fd = undefined; - }); } + async close(): Promise { - await this.fd?.close(); + 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) { @@ -49,18 +57,29 @@ class FileReader extends Reader { 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 { - super.close(); - await this.reader.close(); + if (this.closed) return; + this.closed = true; + + try { + await super.close(); + } finally { + await this.reader.close(); + } } }