@@ -35,6 +35,7 @@ import {
|
||||
import { getOSPlatform, isCJKEnv, isContentURI, isValidURL } from '@/utils/misc';
|
||||
import { deserializeConfig, serializeConfig } from '@/utils/serializer';
|
||||
import { downloadFile, uploadFile, deleteFile, createProgressHandler } from '@/libs/storage';
|
||||
import { ClosableFile } from '@/utils/file';
|
||||
import { ProgressHandler } from '@/utils/transfer';
|
||||
import { TxtToEpubConverter } from '@/utils/txt';
|
||||
import { BOOK_FILE_NOT_FOUND_ERROR } from './errors';
|
||||
@@ -244,6 +245,10 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
}
|
||||
book.coverImageUrl = await this.generateCoverImageUrl(book);
|
||||
const f = file as ClosableFile;
|
||||
if (f && f.close) {
|
||||
await f.close();
|
||||
}
|
||||
|
||||
return book;
|
||||
} catch (error) {
|
||||
@@ -282,6 +287,10 @@ export abstract class BaseAppService implements AppService {
|
||||
const file = await this.fs.openFile(lfp, 'Books', cfp);
|
||||
const localFullpath = `${this.localBooksDir}/${lfp}`;
|
||||
await uploadFile(file, localFullpath, handleProgress, hash);
|
||||
const f = file as ClosableFile;
|
||||
if (f && f.close) {
|
||||
await f.close();
|
||||
}
|
||||
}
|
||||
|
||||
async uploadBook(book: Book, onProgress?: ProgressHandler): Promise<void> {
|
||||
@@ -421,6 +430,10 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
const { file } = (await this.loadBookContent(book, settings)) as BookContent;
|
||||
const bookDoc = (await new DocumentLoader(file).open()).book as BookDoc;
|
||||
const f = file as ClosableFile;
|
||||
if (f && f.close) {
|
||||
await f.close();
|
||||
}
|
||||
return bookDoc.metadata;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,12 @@ class DeferredBlob extends Blob {
|
||||
}
|
||||
}
|
||||
|
||||
export class NativeFile extends File {
|
||||
export interface ClosableFile extends File {
|
||||
open(): Promise<this>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export class NativeFile extends File implements ClosableFile {
|
||||
#handle: FileHandle | null = null;
|
||||
#fp: string;
|
||||
#name: string;
|
||||
@@ -59,6 +64,11 @@ export class NativeFile extends File {
|
||||
#size: number = -1;
|
||||
#type: string = '';
|
||||
|
||||
static MAX_CACHE_CHUNK_SIZE = 1024 * 1024;
|
||||
static MAX_CACHE_ITEMS_SIZE = 20;
|
||||
#cache: Map<number, ArrayBuffer> = new Map();
|
||||
#order: number[] = [];
|
||||
|
||||
constructor(fp: string, name?: string, baseDir: BaseDirectory | null = null, type = '') {
|
||||
super([], name || fp, { type });
|
||||
this.#fp = fp;
|
||||
@@ -74,6 +84,15 @@ export class NativeFile extends File {
|
||||
return this;
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.#handle) {
|
||||
await this.#handle.close();
|
||||
this.#handle = null;
|
||||
}
|
||||
this.#cache.clear();
|
||||
this.#order = [];
|
||||
}
|
||||
|
||||
override get name() {
|
||||
return this.#name;
|
||||
}
|
||||
@@ -94,10 +113,6 @@ export class NativeFile extends File {
|
||||
return this.#handle?.stat();
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
return this.#handle?.close();
|
||||
}
|
||||
|
||||
async seek(offset: number, whence: SeekMode): Promise<number> {
|
||||
if (!this.#handle) {
|
||||
throw new Error('File handle is not open');
|
||||
@@ -112,10 +127,58 @@ export class NativeFile extends File {
|
||||
}
|
||||
start = Math.max(0, start);
|
||||
end = Math.max(start, Math.min(this.size, end));
|
||||
await this.#handle.seek(start, SeekMode.Start);
|
||||
const buffer = new Uint8Array(end - start);
|
||||
const size = end - start;
|
||||
|
||||
if (size > NativeFile.MAX_CACHE_CHUNK_SIZE) {
|
||||
await this.#handle.seek(start, SeekMode.Start);
|
||||
const buffer = new Uint8Array(size);
|
||||
await this.#handle.read(buffer);
|
||||
return buffer.buffer;
|
||||
}
|
||||
|
||||
const cachedChunkStart = Array.from(this.#cache.keys()).find((chunkStart) => {
|
||||
const buffer = this.#cache.get(chunkStart)!;
|
||||
return start >= chunkStart && end <= chunkStart + buffer.byteLength;
|
||||
});
|
||||
|
||||
if (cachedChunkStart !== undefined) {
|
||||
this.#updateAccessOrder(cachedChunkStart);
|
||||
const buffer = this.#cache.get(cachedChunkStart)!;
|
||||
const offset = start - cachedChunkStart;
|
||||
return buffer.slice(offset, offset + size);
|
||||
}
|
||||
|
||||
const chunkStart = Math.max(0, start - 1024);
|
||||
const chunkEnd = Math.min(this.size, chunkStart + NativeFile.MAX_CACHE_CHUNK_SIZE);
|
||||
const chunkSize = chunkEnd - chunkStart;
|
||||
|
||||
await this.#handle.seek(chunkStart, SeekMode.Start);
|
||||
const buffer = new Uint8Array(chunkSize);
|
||||
await this.#handle.read(buffer);
|
||||
return buffer.buffer;
|
||||
|
||||
this.#cache.set(chunkStart, buffer.buffer);
|
||||
this.#updateAccessOrder(chunkStart);
|
||||
this.#ensureCacheSize();
|
||||
|
||||
const offset = start - chunkStart;
|
||||
return buffer.buffer.slice(offset, offset + size);
|
||||
}
|
||||
|
||||
#updateAccessOrder(chunkStart: number) {
|
||||
const index = this.#order.indexOf(chunkStart);
|
||||
if (index > -1) {
|
||||
this.#order.splice(index, 1);
|
||||
}
|
||||
this.#order.unshift(chunkStart);
|
||||
}
|
||||
|
||||
#ensureCacheSize() {
|
||||
while (this.#cache.size > NativeFile.MAX_CACHE_ITEMS_SIZE) {
|
||||
const oldestKey = this.#order.pop();
|
||||
if (oldestKey !== undefined) {
|
||||
this.#cache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override slice(start = 0, end = this.size, contentType = this.type): Blob {
|
||||
@@ -172,7 +235,7 @@ export class NativeFile extends File {
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteFile extends File {
|
||||
export class RemoteFile extends File implements ClosableFile {
|
||||
url: string;
|
||||
#name: string;
|
||||
#lastModified: number;
|
||||
@@ -238,7 +301,10 @@ export class RemoteFile extends File {
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
async close(): Promise<void> {
|
||||
this.#cache.clear();
|
||||
this.#order = [];
|
||||
}
|
||||
|
||||
async fetchRangePart(start: number, end: number) {
|
||||
start = Math.max(0, start);
|
||||
|
||||
Reference in New Issue
Block a user