From c3a81a1294e31f982f72fc5b51da4d30ea9d53bf Mon Sep 17 00:00:00 2001 From: Roy Hashimoto Date: Tue, 11 Jun 2024 10:56:57 -0700 Subject: [PATCH] Add OPFSAnyContextVFS. --- demo/demo-worker.js | 5 + src/examples/OPFSAnyContextVFS.js | 300 ++++++++++++++++++ ...ateVFS.test.js => OPFSAdaptiveVFS.test.js} | 0 test/OPFSAnyContextVFS.test.js | 27 ++ test/api.test.js | 1 + test/sql.test.js | 1 + test/test-worker.js | 4 + 7 files changed, 338 insertions(+) create mode 100644 src/examples/OPFSAnyContextVFS.js rename test/{OriginPrivateVFS.test.js => OPFSAdaptiveVFS.test.js} (100%) create mode 100644 test/OPFSAnyContextVFS.test.js diff --git a/demo/demo-worker.js b/demo/demo-worker.js index 2964c8e..5057f3f 100644 --- a/demo/demo-worker.js +++ b/demo/demo-worker.js @@ -42,6 +42,11 @@ const BUILDS = new Map([ vfsModule: '../src/examples/OPFSAdaptiveVFS.js', vfsOptions: { lockPolicy: 'shared+hint' } }, + { + name: 'OPFSAnyContextVFS', + vfsModule: '../src/examples/OPFSAnyContextVFS.js', + vfsOptions: { lockPolicy: 'shared+hint' } + }, { name: 'OPFSCoopSyncVFS', vfsModule: '../src/examples/OPFSCoopSyncVFS.js', diff --git a/src/examples/OPFSAnyContextVFS.js b/src/examples/OPFSAnyContextVFS.js new file mode 100644 index 0000000..9cc6a67 --- /dev/null +++ b/src/examples/OPFSAnyContextVFS.js @@ -0,0 +1,300 @@ +// Copyright 2024 Roy T. Hashimoto. All Rights Reserved. +import { FacadeVFS } from '../FacadeVFS.js'; +import * as VFS from '../VFS.js'; +import { WebLocksMixin } from '../WebLocksMixin.js'; + +/** + * @param {string} pathname + * @param {boolean} create + * @returns {Promise<[FileSystemDirectoryHandle, string]>} + */ +async function getPathComponents(pathname, create) { + const [_, directories, filename] = pathname.match(/[/]?(.*)[/](.*)$/); + + let directoryHandle = await navigator.storage.getDirectory(); + for (const directory of directories.split('/')) { + if (directory) { + directoryHandle = await directoryHandle.getDirectoryHandle(directory, { create }); + } + } + return [directoryHandle, filename]; +}; + +class File { + /** @type {string} */ pathname; + /** @type {number} */ flags; + /** @type {FileSystemFileHandle} */ fileHandle; + /** @type {Blob?} */ blob; + /** @type {FileSystemWritableFileStream?} */ writable; + + constructor(pathname, flags) { + this.pathname = pathname; + this.flags = flags; + } +} + +export class OPFSAnyContextVFS extends WebLocksMixin(FacadeVFS) { + /** @type {Map} */ mapIdToFile = new Map(); + lastError = null; + + log = null; + + static async create(name, module, options) { + const vfs = new OPFSAnyContextVFS(name, module, options); + await vfs.isReady(); + return vfs; + } + + constructor(name, module, options = {}) { + super(name, module, options); + } + + getFilename(fileId) { + const pathname = this.mapIdToFile.get(fileId).pathname; + return `OPFS:${pathname}` + } + + /** + * @param {string?} zName + * @param {number} fileId + * @param {number} flags + * @param {DataView} pOutFlags + * @returns {Promise} + */ + async jOpen(zName, fileId, flags, pOutFlags) { + try { + const url = new URL(zName || Math.random().toString(36).slice(2), 'file://'); + const pathname = url.pathname; + + const file = new File(pathname, flags); + this.mapIdToFile.set(fileId, file); + + const create = !!(flags & VFS.SQLITE_OPEN_CREATE); + const [directoryHandle, filename] = await getPathComponents(pathname, create); + file.fileHandle = await directoryHandle.getFileHandle(filename, { create }); + + pOutFlags.setInt32(0, flags, true); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_CANTOPEN; + } + } + + /** + * @param {string} zName + * @param {number} syncDir + * @returns {Promise} + */ + async jDelete(zName, syncDir) { + try { + const url = new URL(zName, 'file://'); + const pathname = url.pathname; + + const [directoryHandle, name] = await getPathComponents(pathname, false); + const result = directoryHandle.removeEntry(name, { recursive: false }); + if (syncDir) { + await result; + } + return VFS.SQLITE_OK; + } catch (e) { + return VFS.SQLITE_IOERR_DELETE; + } + } + + /** + * @param {string} zName + * @param {number} flags + * @param {DataView} pResOut + * @returns {Promise} + */ + async jAccess(zName, flags, pResOut) { + try { + const url = new URL(zName, 'file://'); + const pathname = url.pathname; + + const [directoryHandle, dbName] = await getPathComponents(pathname, false); + const fileHandle = await directoryHandle.getFileHandle(dbName, { create: false }); + pResOut.setInt32(0, 1, true); + return VFS.SQLITE_OK; + } catch (e) { + if (e.name === 'NotFoundError') { + pResOut.setInt32(0, 0, true); + return VFS.SQLITE_OK; + } + this.lastError = e; + return VFS.SQLITE_IOERR_ACCESS; + } + } + + /** + * @param {number} fileId + * @returns {Promise} + */ + async jClose(fileId) { + try { + const file = this.mapIdToFile.get(fileId); + this.mapIdToFile.delete(fileId); + + await file.writable?.close(); + if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) { + const [directoryHandle, name] = await getPathComponents(file.pathname, false); + await directoryHandle.removeEntry(name, { recursive: false }); + } + return VFS.SQLITE_OK; + } catch (e) { + return VFS.SQLITE_IOERR_DELETE; + } + } + + /** + * @param {number} fileId + * @param {Uint8Array} pData + * @param {number} iOffset + * @returns {Promise} + */ + async jRead(fileId, pData, iOffset) { + try { + const file = this.mapIdToFile.get(fileId); + + if (file.writable) { + await file.writable.close(); + file.writable = null; + file.blob = null; + } + if (!file.blob) { + file.blob = await file.fileHandle.getFile(); + } + + const bytesRead = await file.blob.slice(iOffset, iOffset + pData.byteLength) + .arrayBuffer() + .then(arrayBuffer => { + pData.set(new Uint8Array(arrayBuffer)); + return arrayBuffer.byteLength; + }); + + if (bytesRead < pData.byteLength) { + pData.fill(0, bytesRead); + return VFS.SQLITE_IOERR_SHORT_READ; + } + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_READ; + } + } + + /** + * @param {number} fileId + * @param {Uint8Array} pData + * @param {number} iOffset + * @returns {Promise} + */ + async jWrite(fileId, pData, iOffset) { + try { + const file = this.mapIdToFile.get(fileId); + + if (!file.writable) { + file.writable = await file.fileHandle.createWritable({ keepExistingData: true }); + } + await file.writable.seek(iOffset); + await file.writable.write(pData.subarray()); + file.blob = null; + + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_WRITE; + } + } + + /** + * @param {number} fileId + * @param {number} iSize + * @returns {Promise} + */ + async jTruncate(fileId, iSize) { + try { + const file = this.mapIdToFile.get(fileId); + + if (!file.writable) { + file.writable = await file.fileHandle.createWritable({ keepExistingData: true }); + } + await file.writable.truncate(iSize); + file.blob = null; + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_TRUNCATE; + } + } + + /** + * @param {number} fileId + * @param {number} flags + * @returns {Promise} + */ + async jSync(fileId, flags) { + try { + const file = this.mapIdToFile.get(fileId); + await file.writable?.close(); + file.writable = null; + file.blob = null; + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_FSYNC; + } + } + + /** + * @param {number} fileId + * @param {DataView} pSize64 + * @returns {Promise} + */ + async jFileSize(fileId, pSize64) { + try { + const file = this.mapIdToFile.get(fileId); + + if (file.writable) { + await file.writable.close(); + file.writable = null; + file.blob = null; + } + if (!file.blob) { + file.blob = await file.fileHandle.getFile(); + } + pSize64.setBigInt64(0, BigInt(file.blob.size), true); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_FSTAT; + } + } + + /** + * @param {number} fileId + * @param {number} lockType + * @returns {Promise} + */ + async jLock(fileId, lockType) { + if (lockType === VFS.SQLITE_LOCK_SHARED) { + // Make sure to get a current readable view of the file. + const file = this.mapIdToFile.get(fileId); + file.blob = null; + } + + // Call the actual unlock implementation. + return super.jLock(fileId, lockType); + } + + jGetLastError(zBuf) { + if (this.lastError) { + console.error(this.lastError); + const outputArray = zBuf.subarray(0, zBuf.byteLength - 1); + const { written } = new TextEncoder().encodeInto(this.lastError.message, outputArray); + zBuf[written] = 0; + } + return VFS.SQLITE_OK + } +} diff --git a/test/OriginPrivateVFS.test.js b/test/OPFSAdaptiveVFS.test.js similarity index 100% rename from test/OriginPrivateVFS.test.js rename to test/OPFSAdaptiveVFS.test.js diff --git a/test/OPFSAnyContextVFS.test.js b/test/OPFSAnyContextVFS.test.js new file mode 100644 index 0000000..ca0c534 --- /dev/null +++ b/test/OPFSAnyContextVFS.test.js @@ -0,0 +1,27 @@ +import { TestContext } from "./TestContext.js"; +import { vfs_xOpen } from "./vfs_xOpen.js"; +import { vfs_xAccess } from "./vfs_xAccess.js"; +import { vfs_xClose } from "./vfs_xClose.js"; +import { vfs_xRead } from "./vfs_xRead.js"; +import { vfs_xWrite } from "./vfs_xWrite.js"; + +const CONFIG = 'OPFSAnyContextVFS'; +const BUILDS = ['asyncify', 'jspi']; + +const supportsJSPI = await TestContext.supportsJSPI(); + +describe(CONFIG, function() { + for (const build of BUILDS) { + if (build === 'jspi' && !supportsJSPI) return; + + describe(build, function() { + const context = new TestContext({ build, config: CONFIG }); + + vfs_xAccess(context); + vfs_xOpen(context); + vfs_xClose(context); + vfs_xRead(context); + vfs_xWrite(context); + }); + } +}); diff --git a/test/api.test.js b/test/api.test.js index 206d7b9..627aafa 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -21,6 +21,7 @@ const CONFIGS = new Map([ ['MemoryAsyncVFS', ASYNC_BUILDS], ['IDBBatchAtomicVFS', ASYNC_BUILDS], ['OPFSAdaptiveVFS', ASYNC_BUILDS], + ['OPFSAnyContextVFS', ASYNC_BUILDS], ['OPFSPermutedVFS', ASYNC_BUILDS], ]); diff --git a/test/sql.test.js b/test/sql.test.js index e12c04e..5778c2b 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -20,6 +20,7 @@ const CONFIGS = new Map([ ['MemoryAsyncVFS', ASYNC_BUILDS], ['IDBBatchAtomicVFS', ASYNC_BUILDS], ['OPFSAdaptiveVFS', ASYNC_BUILDS], + ['OPFSAnyContextVFS', ASYNC_BUILDS], ['OPFSPermutedVFS', ASYNC_BUILDS], ]); diff --git a/test/test-worker.js b/test/test-worker.js index cec5789..44b1663 100644 --- a/test/test-worker.js +++ b/test/test-worker.js @@ -43,6 +43,10 @@ const VFS_CONFIGS = new Map([ name: 'OPFSAdaptiveVFS', vfsModule: '../src/examples/OPFSAdaptiveVFS.js', }, + { + name: 'OPFSAnyContextVFS', + vfsModule: '../src/examples/OPFSAnyContextVFS.js', + }, { name: 'OPFSPermutedVFS', vfsModule: '../src/examples/OPFSPermutedVFS.js',