diff --git a/packages/duckdb-wasm/src/bindings/bindings_base.ts b/packages/duckdb-wasm/src/bindings/bindings_base.ts index b33524e4f..f395bdb10 100644 --- a/packages/duckdb-wasm/src/bindings/bindings_base.ts +++ b/packages/duckdb-wasm/src/bindings/bindings_base.ts @@ -469,6 +469,19 @@ export abstract class DuckDBBindingsBase implements DuckDBBindings { } dropResponseBuffers(this.mod); } + public async prepareFileHandle(fileName: string, protocol: DuckDBDataProtocol): Promise { + if (protocol === DuckDBDataProtocol.BROWSER_FSACCESS && this._runtime.prepareFileHandles) { + const list = await this._runtime.prepareFileHandles([fileName], DuckDBDataProtocol.BROWSER_FSACCESS); + for (const item of list) { + const { handle, path: filePath, fromCached } = item; + if (!fromCached && handle.getSize()) { + await this.registerFileHandleAsync(filePath, handle, DuckDBDataProtocol.BROWSER_FSACCESS, true); + } + } + return; + } + throw new Error(`prepareFileHandle: unsupported protocol ${protocol}`); + } /** Prepare a file handle that could only be acquired aschronously */ public async prepareDBFileHandle(path: string, protocol: DuckDBDataProtocol): Promise { if (protocol === DuckDBDataProtocol.BROWSER_FSACCESS && this._runtime.prepareDBFileHandle) { @@ -601,8 +614,14 @@ export abstract class DuckDBBindingsBase implements DuckDBBindings { dropResponseBuffers(this.mod); return copy; } - /** Enable tracking of file statistics */ + public registerOPFSFileName(file: string): Promise { + if (file.startsWith("opfs://")) { + return this.prepareFileHandle(file, DuckDBDataProtocol.BROWSER_FSACCESS); + } else { + throw new Error("Not an OPFS file name: " + file); + } + } public collectFileStatistics(file: string, enable: boolean): void { const [s, d, n] = callSRet(this.mod, 'duckdb_web_collect_file_stats', ['string', 'boolean'], [file, enable]); if (s !== StatusCode.SUCCESS) { diff --git a/packages/duckdb-wasm/src/bindings/bindings_interface.ts b/packages/duckdb-wasm/src/bindings/bindings_interface.ts index dcf0fb926..a565facbb 100644 --- a/packages/duckdb-wasm/src/bindings/bindings_interface.ts +++ b/packages/duckdb-wasm/src/bindings/bindings_interface.ts @@ -54,6 +54,7 @@ export interface DuckDBBindings { protocol: DuckDBDataProtocol, directIO: boolean, ): Promise; + prepareFileHandle(path: string, protocol: DuckDBDataProtocol): Promise; prepareDBFileHandle(path: string, protocol: DuckDBDataProtocol): Promise; globFiles(path: string): WebFile[]; dropFile(name: string): void; diff --git a/packages/duckdb-wasm/src/bindings/runtime.ts b/packages/duckdb-wasm/src/bindings/runtime.ts index 4bc360db3..b720e6877 100644 --- a/packages/duckdb-wasm/src/bindings/runtime.ts +++ b/packages/duckdb-wasm/src/bindings/runtime.ts @@ -157,6 +157,8 @@ export interface DuckDBRuntime { removeFile(mod: DuckDBModule, pathPtr: number, pathLen: number): void; // Prepare a file handle that could only be acquired aschronously + prepareFileHandle?: (path: string, protocol: DuckDBDataProtocol) => Promise; + prepareFileHandles?: (path: string[], protocol: DuckDBDataProtocol) => Promise; prepareDBFileHandle?: (path: string, protocol: DuckDBDataProtocol) => Promise; // Call a scalar UDF function diff --git a/packages/duckdb-wasm/src/bindings/runtime_browser.ts b/packages/duckdb-wasm/src/bindings/runtime_browser.ts index 8099acde7..0b4ebedca 100644 --- a/packages/duckdb-wasm/src/bindings/runtime_browser.ts +++ b/packages/duckdb-wasm/src/bindings/runtime_browser.ts @@ -19,20 +19,24 @@ import * as udf from './udf_runtime'; const OPFS_PREFIX_LEN = 'opfs://'.length; const PATH_SEP_REGEX = /\/|\\/; + export const BROWSER_RUNTIME: DuckDBRuntime & { _files: Map; _fileInfoCache: Map; _globalFileInfo: DuckDBGlobalFileInfo | null; _preparedHandles: Record; + _opfsRoot: FileSystemDirectoryHandle | null; getFileInfo(mod: DuckDBModule, fileId: number): DuckDBFileInfo | null; getGlobalFileInfo(mod: DuckDBModule): DuckDBGlobalFileInfo | null; + assignOPFSRoot(): Promise; } = { _files: new Map(), _fileInfoCache: new Map(), _udfFunctions: new Map(), _globalFileInfo: null, _preparedHandles: {} as any, + _opfsRoot: null, getFileInfo(mod: DuckDBModule, fileId: number): DuckDBFileInfo | null { try { @@ -101,11 +105,15 @@ export const BROWSER_RUNTIME: DuckDBRuntime & { return null; } }, - + async assignOPFSRoot(): Promise { + if (!BROWSER_RUNTIME._opfsRoot) { + BROWSER_RUNTIME._opfsRoot = await navigator.storage.getDirectory(); + } + }, /** Prepare a file handle that could only be acquired aschronously */ - async prepareDBFileHandle(dbPath: string, protocol: DuckDBDataProtocol): Promise { + async prepareFileHandles(filePaths: string[], protocol: DuckDBDataProtocol): Promise { if (protocol === DuckDBDataProtocol.BROWSER_FSACCESS) { - const filePaths = [dbPath, `${dbPath}.wal`]; + await BROWSER_RUNTIME.assignOPFSRoot(); const prepare = async (path: string): Promise => { if (BROWSER_RUNTIME._files.has(path)) { return { @@ -114,7 +122,7 @@ export const BROWSER_RUNTIME: DuckDBRuntime & { fromCached: true, }; } - const opfsRoot = await navigator.storage.getDirectory(); + const opfsRoot = BROWSER_RUNTIME._opfsRoot!; let dirHandle: FileSystemDirectoryHandle = opfsRoot; // check if mkdir -p is needed const opfsPath = path.slice(OPFS_PREFIX_LEN); @@ -156,6 +164,14 @@ export const BROWSER_RUNTIME: DuckDBRuntime & { } return result; } + throw new Error(`Unsupported protocol ${protocol} for paths ${filePaths} with protocol ${protocol}`); + }, + /** Prepare a file handle that could only be acquired aschronously */ + async prepareDBFileHandle(dbPath: string, protocol: DuckDBDataProtocol): Promise { + if (protocol === DuckDBDataProtocol.BROWSER_FSACCESS && this.prepareFileHandles) { + const filePaths = [dbPath, `${dbPath}.wal`]; + return this.prepareFileHandles(filePaths, protocol); + } throw new Error(`Unsupported protocol ${protocol} for path ${dbPath} with protocol ${protocol}`); },