From 5850a16afd2351e2ecaf4c9738a4ccc5d6eea322 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Tue, 16 Dec 2025 13:51:48 +0800 Subject: [PATCH] fix: add stats API and fix fd leak, closes #2323 (#2723) --- apps/readest-app/src/services/appService.ts | 17 ++++----- .../src/services/nativeAppService.ts | 5 +++ .../readest-app/src/services/webAppService.ts | 37 +++++++++++++++++++ apps/readest-app/src/types/system.ts | 10 +++++ 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/apps/readest-app/src/services/appService.ts b/apps/readest-app/src/services/appService.ts index 300e1699..95e6225b 100644 --- a/apps/readest-app/src/services/appService.ts +++ b/apps/readest-app/src/services/appService.ts @@ -396,26 +396,23 @@ export abstract class BaseAppService implements AppService { if (!(await this.fs.exists(getDir(book), 'Books'))) { await this.fs.createDir(getDir(book), 'Books'); } - if ( - saveBook && - !transient && - (!(await this.fs.exists(getLocalBookFilename(book), 'Books')) || overwrite) - ) { + const bookFilename = getLocalBookFilename(book); + if (saveBook && !transient && (!(await this.fs.exists(bookFilename, 'Books')) || overwrite)) { if (/\.txt$/i.test(filename)) { - await this.fs.writeFile(getLocalBookFilename(book), 'Books', fileobj); + await this.fs.writeFile(bookFilename, 'Books', fileobj); } else if (typeof file === 'string' && isContentURI(file)) { - await this.fs.copyFile(file, getLocalBookFilename(book), 'Books'); + await this.fs.copyFile(file, bookFilename, 'Books'); } else if (typeof file === 'string' && !isValidURL(file)) { try { // try to copy the file directly first in case of large files to avoid memory issues // on desktop when reading recursively from selected directory the direct copy will fail // due to permission issues, then fallback to read and write files - await this.fs.copyFile(file, getLocalBookFilename(book), 'Books'); + await this.fs.copyFile(file, bookFilename, 'Books'); } catch { - await this.fs.writeFile(getLocalBookFilename(book), 'Books', fileobj); + await this.fs.writeFile(bookFilename, 'Books', await fileobj.arrayBuffer()); } } else { - await this.fs.writeFile(getLocalBookFilename(book), 'Books', fileobj); + await this.fs.writeFile(bookFilename, 'Books', fileobj); } } if (saveCover && (!(await this.fs.exists(getCoverFilename(book), 'Books')) || overwrite)) { diff --git a/apps/readest-app/src/services/nativeAppService.ts b/apps/readest-app/src/services/nativeAppService.ts index 471acadd..9c437d1c 100644 --- a/apps/readest-app/src/services/nativeAppService.ts +++ b/apps/readest-app/src/services/nativeAppService.ts @@ -347,6 +347,11 @@ export const nativeFileSystem: FileSystem = { return false; } }, + async stats(path: string, base: BaseDir) { + const { fp, baseDir } = this.resolvePath(path, base); + + return await stat(fp, baseDir ? { baseDir } : undefined); + }, }; const DIST_CHANNEL = (process.env['NEXT_PUBLIC_DIST_CHANNEL'] || 'readest') as DistChannel; diff --git a/apps/readest-app/src/services/webAppService.ts b/apps/readest-app/src/services/webAppService.ts index a15ceb89..552bdf88 100644 --- a/apps/readest-app/src/services/webAppService.ts +++ b/apps/readest-app/src/services/webAppService.ts @@ -218,6 +218,43 @@ const indexedDBFileSystem: FileSystem = { request.onerror = () => reject(request.error); }); }, + async stats(path: string, base: BaseDir) { + const { fp } = this.resolvePath(path, base); + const db = await openIndexedDB(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction('files', 'readonly'); + const store = transaction.objectStore('files'); + const request = store.get(fp); + + request.onsuccess = () => { + const result = request.result; + if (result) { + const content = result.content; + const size = + content instanceof Blob + ? content.size + : typeof content === 'string' + ? content.length + : content instanceof ArrayBuffer + ? content.byteLength + : 0; + resolve({ + isFile: true, + isDirectory: false, + size, + mtime: null, + atime: null, + birthtime: null, + }); + } else { + reject(new Error(`File not found: ${fp}`)); + } + }; + + request.onerror = () => reject(request.error); + }); + }, }; export class WebAppService extends BaseAppService { diff --git a/apps/readest-app/src/types/system.ts b/apps/readest-app/src/types/system.ts index d69b4271..b85a32f0 100644 --- a/apps/readest-app/src/types/system.ts +++ b/apps/readest-app/src/types/system.ts @@ -25,6 +25,15 @@ export type FileItem = { size: number; }; +export type FileInfo = { + isFile: boolean; + isDirectory: boolean; + size: number; + mtime: Date | null; + atime: Date | null; + birthtime: Date | null; +}; + export interface FileSystem { resolvePath(path: string, base: BaseDir): ResolvedPath; getURL(path: string): string; @@ -39,6 +48,7 @@ export interface FileSystem { createDir(path: string, base: BaseDir, recursive?: boolean): Promise; removeDir(path: string, base: BaseDir, recursive?: boolean): Promise; exists(path: string, base: BaseDir): Promise; + stats(path: string, base: BaseDir): Promise; getPrefix(base: BaseDir): Promise; }