Skip to content

Commit

Permalink
Add OPFSAnyContextVFS.
Browse files Browse the repository at this point in the history
  • Loading branch information
shoestringresearch committed Jun 11, 2024
1 parent a05f863 commit c3a81a1
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 0 deletions.
5 changes: 5 additions & 0 deletions demo/demo-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
300 changes: 300 additions & 0 deletions src/examples/OPFSAnyContextVFS.js
Original file line number Diff line number Diff line change
@@ -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<number, File>} */ 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<number>}
*/
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<number>}
*/
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<number>}
*/
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<number>}
*/
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<number>}
*/
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<number>}
*/
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<number>}
*/
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<number>}
*/
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<number>}
*/
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<number>}
*/
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
}
}
File renamed without changes.
27 changes: 27 additions & 0 deletions test/OPFSAnyContextVFS.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
}
});
1 change: 1 addition & 0 deletions test/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const CONFIGS = new Map([
['MemoryAsyncVFS', ASYNC_BUILDS],
['IDBBatchAtomicVFS', ASYNC_BUILDS],
['OPFSAdaptiveVFS', ASYNC_BUILDS],
['OPFSAnyContextVFS', ASYNC_BUILDS],
['OPFSPermutedVFS', ASYNC_BUILDS],
]);

Expand Down
1 change: 1 addition & 0 deletions test/sql.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const CONFIGS = new Map([
['MemoryAsyncVFS', ASYNC_BUILDS],
['IDBBatchAtomicVFS', ASYNC_BUILDS],
['OPFSAdaptiveVFS', ASYNC_BUILDS],
['OPFSAnyContextVFS', ASYNC_BUILDS],
['OPFSPermutedVFS', ASYNC_BUILDS],
]);

Expand Down
4 changes: 4 additions & 0 deletions test/test-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit c3a81a1

Please sign in to comment.