diff options
Diffstat (limited to 'ext/wasm/api')
-rw-r--r-- | ext/wasm/api/sqlite3-api-oo1.js | 25 | ||||
-rw-r--r-- | ext/wasm/api/sqlite3-api-opfs.js | 238 | ||||
-rw-r--r-- | ext/wasm/api/sqlite3-api-prologue.js | 21 | ||||
-rw-r--r-- | ext/wasm/api/sqlite3-api-worker1.js | 23 | ||||
-rw-r--r-- | ext/wasm/api/sqlite3-opfs-async-proxy.js | 1 |
5 files changed, 257 insertions, 51 deletions
diff --git a/ext/wasm/api/sqlite3-api-oo1.js b/ext/wasm/api/sqlite3-api-oo1.js index e318b7200..aaaeaa3d5 100644 --- a/ext/wasm/api/sqlite3-api-oo1.js +++ b/ext/wasm/api/sqlite3-api-oo1.js @@ -258,9 +258,9 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ in the form of a single configuration object with the following properties: - - `.filename`: database file name - - `.flags`: open-mode flags - - `.vfs`: the VFS fname + - `filename`: database file name + - `flags`: open-mode flags + - `vfs`: the VFS fname The `filename` and `vfs` arguments may be either JS strings or C-strings allocated via WASM. `flags` is required to be a JS @@ -561,6 +561,25 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ return capi.sqlite3_db_name(affirmDbOpen(this).pointer, dbNumber); }, /** + Returns the name of the sqlite3_vfs used by the given database + of this connection (defaulting to 'main'). The argument may be + either a JS string or a WASM C-string. Returns undefined if the + given db name is invalid. Throws if this object has been + close()d. + */ + dbVfsName: function(dbName=0){ + let rc; + const pVfs = capi.sqlite3_js_db_vfs( + affirmDbOpen(this).pointer, dbName + ); + if(pVfs){ + const v = new capi.sqlite3_vfs(pVfs); + try{ rc = wasm.cstringToJs(v.$zName) } + finally { v.dispose() } + } + return rc; + }, + /** Compiles the given SQL and returns a prepared Stmt. This is the only way to create new Stmt objects. Throws on error. diff --git a/ext/wasm/api/sqlite3-api-opfs.js b/ext/wasm/api/sqlite3-api-opfs.js index 1a89432c5..90337d527 100644 --- a/ext/wasm/api/sqlite3-api-opfs.js +++ b/ext/wasm/api/sqlite3-api-opfs.js @@ -371,6 +371,20 @@ const installOpfsVfs = function callee(options){ return rc; }; + /** + Not part of the public API. Only for test/development use. + */ + opfsUtil.debug = { + asyncShutdown: ()=>{ + warn("Shutting down OPFS async listener. The OPFS VFS will no longer work."); + opRun('opfs-async-shutdown'); + }, + asyncRestart: ()=>{ + warn("Attempting to restart OPFS VFS async listener. Might work, might not."); + W.postMessage({type: 'opfs-async-restart'}); + } + }; + const initS11n = ()=>{ /** !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -876,37 +890,61 @@ const installOpfsVfs = function callee(options){ } /** - Syncronously deletes the given OPFS filesystem entry, ignoring - any errors. As this environment has no notion of "current - directory", the given name must be an absolute path. If the 2nd - argument is truthy, deletion is recursive (use with caution!). + Expects an OPFS file path. It gets resolved, such that ".." + components are properly expanded, and returned. If the 2nd arg + is true, the result is returned as an array of path elements, + else an absolute path string is returned. + */ + opfsUtil.getResolvedPath = function(filename,splitIt){ + const p = new URL(filename, "file://irrelevant").pathname; + return splitIt ? p.split('/').filter((v)=>!!v) : p; + }; - Returns true if the deletion succeeded and false if it fails, - but cannot report the nature of the failure. + /** + Takes the absolute path to a filesystem element. Returns an + array of [handleOfContainingDir, filename]. If the 2nd argument + is truthy then each directory element leading to the file is + created along the way. Throws if any creation or resolution + fails. */ - opfsUtil.deleteEntry = function(fsEntryName,recursive=false){ - mTimeStart('xDelete'); - const rc = opRun('xDelete', fsEntryName, 0, recursive); - mTimeEnd(); - return 0===rc; + opfsUtil.getDirForFilename = async function f(absFilename, createDirs = false){ + const path = opfsUtil.getResolvedPath(absFilename, true); + const filename = path.pop(); + let dh = opfsUtil.rootDirectory; + for(const dirName of path){ + if(dirName){ + dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs}); + } + } + return [dh, filename]; }; + /** - Synchronously creates the given directory name, recursively, in + Creates the given directory name, recursively, in the OPFS filesystem. Returns true if it succeeds or the directory already exists, else false. */ - opfsUtil.mkdir = function(absDirName){ - mTimeStart('mkdir'); - const rc = opRun('mkdir', absDirName); - mTimeEnd(); - return 0===rc; + opfsUtil.mkdir = async function(absDirName){ + try { + await opfsUtil.getDirForFilename(absDirName+"/filepart", true); + return true; + }catch(e){ + //console.warn("mkdir(",absDirName,") failed:",e); + return false; + } }; /** - Synchronously checks whether the given OPFS filesystem exists, + Checks whether the given OPFS filesystem entry exists, returning true if it does, false if it doesn't. */ - opfsUtil.entryExists = function(fsEntryName){ - return 0===opRun('xAccess', fsEntryName); + opfsUtil.entryExists = async function(fsEntryName){ + try { + const [dh, fn] = await opfsUtil.getDirForFilename(filename); + await dh.getFileHandle(fn); + return true; + }catch(e){ + return false; + } }; /** @@ -938,20 +976,160 @@ const installOpfsVfs = function callee(options){ }; /** - Only for test/development use. + Returns a promise which resolves to an object which represents + all files and directories in the OPFS tree. The top-most object + has two properties: `dirs` is an array of directory entries + (described below) and `files` is a list of file names for all + files in that directory. + + Traversal starts at sqlite3.opfs.rootDirectory. + + Each `dirs` entry is an object in this form: + + ``` + { name: directoryName, + dirs: [...subdirs], + files: [...file names] + } + ``` + + The `files` and `subdirs` entries are always set but may be + empty arrays. + + The returned object has the same structure but its `name` is + an empty string. All returned objects are created with + Object.create(null), so have no prototype. + + Design note: the entries do not contain more information, + e.g. file sizes, because getting such info is not only + expensive but is subject to locking-related errors. */ - opfsUtil.debug = { - asyncShutdown: ()=>{ - warn("Shutting down OPFS async listener. OPFS will no longer work."); - opRun('opfs-async-shutdown'); - }, - asyncRestart: ()=>{ - warn("Attempting to restart OPFS async listener. Might work, might not."); - W.postMessage({type: 'opfs-async-restart'}); + opfsUtil.treeList = async function(){ + const doDir = async function callee(dirHandle,tgt){ + tgt.name = dirHandle.name; + tgt.dirs = []; + tgt.files = []; + for await (const handle of dirHandle.values()){ + if('directory' === handle.kind){ + const subDir = Object.create(null); + tgt.dirs.push(subDir); + await callee(handle, subDir); + }else{ + tgt.files.push(handle.name); + } + } + }; + const root = Object.create(null); + await doDir(opfsUtil.rootDirectory, root); + return root; + }; + + /** + Irrevocably deletes _all_ files in the current origin's OPFS. + Obviously, this must be used with great caution. It may throw + an exception if removal of anything fails (e.g. a file is + locked), but the precise conditions under which it will throw + are not documented (so we cannot tell you what they are). + */ + opfsUtil.rmfr = async function(){ + const dir = opfsUtil.rootDirectory, opt = {recurse: true}; + for await (const handle of dir.values()){ + dir.removeEntry(handle.name, opt); + } + }; + + /** + Deletes the given OPFS filesystem entry. As this environment + has no notion of "current directory", the given name must be an + absolute path. If the 2nd argument is truthy, deletion is + recursive (use with caution!). + + The returned Promise resolves to true if the deletion was + successful, else false (but...). The OPFS API reports the + reason for the failure only in human-readable form, not + exceptions which can be type-checked to determine the + failure. Because of that... + + If the final argument is truthy then this function will + propagate any exception on error, rather than returning false. + */ + opfsUtil.unlink = async function(fsEntryName, recursive = false, + throwOnError = false){ + try { + const [hDir, filenamePart] = + await opfsUtil.getDirForFilename(fsEntryName, false); + await hDir.removeEntry(filenamePart, {recursive}); + return true; + }catch(e){ + if(throwOnError){ + throw new Error("unlink(",arguments[0],") failed: "+e.message,{ + cause: e + }); + } + return false; } }; - //TODO to support fiddle db upload: + /** + Traverses the OPFS filesystem, calling a callback for each one. + The argument may be either a callback function or an options object + with any of the following properties: + + - `callback`: function which gets called for each filesystem + entry. It gets passed 3 arguments: 1) the + FileSystemFileHandle or FileSystemDirectoryHandle of each + entry (noting that both are instanceof FileSystemHandle). 2) + the FileSystemDirectoryHandle of the parent directory. 3) the + current depth level, with 0 being at the top of the tree + relative to the starting directory. If the callback returns a + literal false, as opposed to any other falsy value, traversal + stops without an error. Any exceptions it throws are + propagated. Results are undefined if the callback manipulate + the filesystem (e.g. removing or adding entries) because the + how OPFS iterators behave in the face of such changes is + undocumented. + + - `recursive` [bool=true]: specifies whether to recurse into + subdirectories or not. Whether recursion is depth-first or + breadth-first is unspecified! + + - `directory` [FileSystemDirectoryEntry=sqlite3.opfs.rootDirectory] + specifies the starting directory. + + If this function is passed a function, it is assumed to be the + callback. + + Returns a promise because it has to (by virtue of being async) + but that promise has no specific meaning: the traversal it + performs is synchronous. The promise must be used to catch any + exceptions propagated by the callback, however. + + TODO: add an option which specifies whether to traverse + depth-first or breadth-first. We currently do depth-first but + an incremental file browsing widget would benefit more from + breadth-first. + */ + opfsUtil.traverse = async function(opt){ + const defaultOpt = { + recursive: true, + directory: opfsUtil.rootDirectory + }; + if('function'===typeof opt){ + opt = {callback:opt}; + } + opt = Object.assign(defaultOpt, opt||{}); + const doDir = async function callee(dirHandle, depth){ + for await (const handle of dirHandle.values()){ + if(false === opt.callback(handle, dirHandle, depth)) return false; + else if(opt.recursive && 'directory' === handle.kind){ + if(false === await callee(handle, depth + 1)) break; + } + } + }; + doDir(opt.directory, 0); + }; + + //TODO to support fiddle and worker1 db upload: //opfsUtil.createFile = function(absName, content=undefined){...} if(sqlite3.oo1){ diff --git a/ext/wasm/api/sqlite3-api-prologue.js b/ext/wasm/api/sqlite3-api-prologue.js index a5ff0c40a..ca6fad8f8 100644 --- a/ext/wasm/api/sqlite3-api-prologue.js +++ b/ext/wasm/api/sqlite3-api-prologue.js @@ -1187,14 +1187,15 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( } /** - Given an `sqlite3*`, an sqlite3_vfs name, and an optional db - name, returns a truthy value (see below) if that db handle uses - that VFS, else returns false. If pDb is falsy then the 3rd - argument is ignored and this function returns a truthy value if - the default VFS name matches that of the 2nd argument. Results - are undefined if pDb is truthy but refers to an invalid - pointer. The 3rd argument specifies the database name of the - given database connection to check, defaulting to the main db. + Given an `sqlite3*`, an sqlite3_vfs name, and an optional db name + (defaulting to "main"), returns a truthy value (see below) if + that db uses that VFS, else returns false. If pDb is falsy then + the 3rd argument is ignored and this function returns a truthy + value if the default VFS name matches that of the 2nd + argument. Results are undefined if pDb is truthy but refers to an + invalid pointer. The 3rd argument specifies the database name of + the given database connection to check, defaulting to the main + db. The 2nd and 3rd arguments may either be a JS string or a WASM C-string. If the 2nd argument is a NULL WASM pointer, the default @@ -1209,14 +1210,14 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( bad arguments cause a conversion error when passing into wasm-space, false is returned. */ - capi.sqlite3_js_db_uses_vfs = function(pDb,vfsName,dbName="main"){ + capi.sqlite3_js_db_uses_vfs = function(pDb,vfsName,dbName=0){ try{ const pK = capi.sqlite3_vfs_find(vfsName); if(!pK) return false; else if(!pDb){ return pK===capi.sqlite3_vfs_find(0) ? pK : false; }else{ - return pK===capi.sqlite3_js_db_vfs(pDb) ? pK : false; + return pK===capi.sqlite3_js_db_vfs(pDb,dbName) ? pK : false; } }catch(e){ /* Ignore - probably bad args to a wasm-bound function. */ diff --git a/ext/wasm/api/sqlite3-api-worker1.js b/ext/wasm/api/sqlite3-api-worker1.js index aa438a52d..532d61b67 100644 --- a/ext/wasm/api/sqlite3-api-worker1.js +++ b/ext/wasm/api/sqlite3-api-worker1.js @@ -188,6 +188,8 @@ See the sqlite3.oo1.DB constructor for peculiarities and transformations, + vfs: sqlite3_vfs name. Ignored if filename is ":memory:" or "". + This may change how the given filename is resolved. } } ``` @@ -212,6 +214,7 @@ persistent: true if the given filename resides in the known-persistent storage, else false. + vfs: name of the VFS the "main" db is using. } } ``` @@ -362,7 +365,7 @@ sqlite3.initWorker1API = function(){ /** Temp holder for "transferable" postMessage() state. */ xfer: [], open: function(opt){ - const db = new DB(opt.filename); + const db = new DB(opt); this.dbs[getDbId(db)] = db; if(this.dbList.indexOf(db)<0) this.dbList.push(db); return db; @@ -442,12 +445,14 @@ sqlite3.initWorker1API = function(){ oargs.filename = args.filename || ''; }else{ oargs.filename = args.filename; + oargs.vfs = args.vfs; } const db = wState.open(oargs); rc.filename = db.filename; rc.persistent = (!!pDir && db.filename.startsWith(pDir+'/')) || !!sqlite3.capi.sqlite3_js_db_uses_vfs(db.pointer, "opfs"); rc.dbId = getDbId(db); + rc.vfs = db.dbVfsName(); return rc; }, @@ -528,6 +533,7 @@ sqlite3.initWorker1API = function(){ rc.wasmfsOpfsEnabled = !!sqlite3.capi.sqlite3_wasmfs_opfs_dir(); rc.version = sqlite3.version; rc.vfsList = sqlite3.capi.sqlite3_js_vfs_list(); + rc.opfsEnabled = !!sqlite3.opfs; return rc; }, @@ -542,11 +548,6 @@ sqlite3.initWorker1API = function(){ } */ export: function(ev){ - /** - We need to reimplement this to use the Emscripten FS - interface. That part used to be in the OO#1 API but that - dependency was removed from that level of the API. - */ const db = getMsgDb(ev); const response = { bytearray: sqlite3.capi.sqlite3_js_db_export(db.pointer), @@ -559,17 +560,23 @@ sqlite3.initWorker1API = function(){ toss: function(ev){ toss("Testing worker exception"); + }, + + 'opfs-tree': async function(ev){ + if(!sqlite3.opfs) toss("OPFS support is unavailable."); + const response = await sqlite3.opfs.treeList(); + return response; } }/*wMsgHandler*/; - self.onmessage = function(ev){ + self.onmessage = async function(ev){ ev = ev.data; let result, dbId = ev.dbId, evType = ev.type; const arrivalTime = performance.now(); try { if(wMsgHandler.hasOwnProperty(evType) && wMsgHandler[evType] instanceof Function){ - result = wMsgHandler[evType](ev); + result = await wMsgHandler[evType](ev); }else{ toss("Unknown db worker message type:",ev.type); } diff --git a/ext/wasm/api/sqlite3-opfs-async-proxy.js b/ext/wasm/api/sqlite3-opfs-async-proxy.js index 71c822c1f..3aa20fa63 100644 --- a/ext/wasm/api/sqlite3-opfs-async-proxy.js +++ b/ext/wasm/api/sqlite3-opfs-async-proxy.js @@ -377,6 +377,7 @@ const vfsAsyncImpls = { if(!filenamePart) break; await hDir.removeEntry(filenamePart, {recursive}); if(0x1234 !== syncDir) break; + recursive = false; filename = getResolvedPath(filename, true); filename.pop(); filename = filename.join('/'); |