@cereusdb/standard 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -3
- package/dist/external.d.ts +293 -0
- package/dist/external.js +347 -0
- package/dist/index.d.ts +199 -4
- package/dist/index.js +219 -4
- package/dist/storage.d.ts +64 -0
- package/dist/storage.js +211 -0
- package/dist/wasm/cereusdb-external.d.ts +220 -0
- package/dist/wasm/cereusdb-external.js +1428 -0
- package/dist/wasm/cereusdb.d.ts +82 -4
- package/dist/wasm/cereusdb.js +277 -28
- package/dist/wasm/cereusdb_bg.wasm +0 -0
- package/dist/wasm/cereusdb_bg.wasm.d.ts +23 -3
- package/dist/wasm/env_shim.js +52 -4
- package/package.json +5 -1
- /package/dist/wasm/snippets/{cereusdb-object-store-3803cd296f83ecfa → cereusdb-object-store-6ee0e8370430462c}/inline0.js +0 -0
package/dist/index.js
CHANGED
|
@@ -1,4 +1,80 @@
|
|
|
1
1
|
import init, { CereusDB as WasmCereusDB } from './wasm/cereusdb.js';
|
|
2
|
+
import { OPFSStorageBackend } from './storage.js';
|
|
3
|
+
export { MemoryStorageBackend, OPFSStorageBackend, } from './storage.js';
|
|
4
|
+
/** MIME type of GeoParquet exports. */
|
|
5
|
+
export const PARQUET_MIME_TYPE = 'application/vnd.apache.parquet';
|
|
6
|
+
/**
|
|
7
|
+
* Save a file with the browser's download mechanism. Needs a DOM `document`
|
|
8
|
+
* (the main thread); it does not work in Web Workers or Node.js.
|
|
9
|
+
*/
|
|
10
|
+
export function downloadFile(filename, data, mimeType = 'application/octet-stream') {
|
|
11
|
+
if (typeof document === 'undefined') {
|
|
12
|
+
throw new Error('downloadFile() needs a browser document; pass CereusDB.create({ onExport }) to handle exports elsewhere');
|
|
13
|
+
}
|
|
14
|
+
const url = URL.createObjectURL(new Blob([data], { type: mimeType }));
|
|
15
|
+
const link = document.createElement('a');
|
|
16
|
+
link.href = url;
|
|
17
|
+
link.download = filename;
|
|
18
|
+
link.style.display = 'none';
|
|
19
|
+
document.body.appendChild(link);
|
|
20
|
+
try {
|
|
21
|
+
link.click();
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
link.remove();
|
|
25
|
+
// Revoking right away can cancel the download in some browsers.
|
|
26
|
+
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const QUERY_PATTERN = /^\s*(select|with|values|\()/i;
|
|
30
|
+
function resolveExportHandler(option) {
|
|
31
|
+
if (option === false) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (option !== undefined) {
|
|
35
|
+
return option;
|
|
36
|
+
}
|
|
37
|
+
return typeof document !== 'undefined' ? downloadFile : undefined;
|
|
38
|
+
}
|
|
39
|
+
function defaultExportFilename(queryOrTable) {
|
|
40
|
+
if (QUERY_PATTERN.test(queryOrTable)) {
|
|
41
|
+
return 'export.parquet';
|
|
42
|
+
}
|
|
43
|
+
const name = queryOrTable.trim().split('.').pop()?.replace(/^"|"$/g, '');
|
|
44
|
+
return `${name || 'export'}.parquet`;
|
|
45
|
+
}
|
|
46
|
+
function resolveStorageBackends(options) {
|
|
47
|
+
const backends = {};
|
|
48
|
+
for (const [scheme, backend] of Object.entries(options ?? {})) {
|
|
49
|
+
if (backend) {
|
|
50
|
+
backends[scheme.toLowerCase()] = backend;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (options?.opfs === undefined && OPFSStorageBackend.isSupported()) {
|
|
54
|
+
backends.opfs = new OPFSStorageBackend();
|
|
55
|
+
}
|
|
56
|
+
return backends;
|
|
57
|
+
}
|
|
58
|
+
function alterTableClause(operation) {
|
|
59
|
+
if ('renameTo' in operation) {
|
|
60
|
+
return `RENAME TO ${operation.renameTo}`;
|
|
61
|
+
}
|
|
62
|
+
if ('addColumn' in operation) {
|
|
63
|
+
const column = operation.addColumn;
|
|
64
|
+
const notNull = column.notNull ? ' NOT NULL' : '';
|
|
65
|
+
const defaultValue = column.default !== undefined ? ` DEFAULT ${column.default}` : '';
|
|
66
|
+
const ifNotExists = operation.ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
67
|
+
return `ADD COLUMN ${ifNotExists}${column.name} ${column.type}${notNull}${defaultValue}`;
|
|
68
|
+
}
|
|
69
|
+
if ('dropColumn' in operation) {
|
|
70
|
+
const columns = Array.isArray(operation.dropColumn)
|
|
71
|
+
? operation.dropColumn
|
|
72
|
+
: [operation.dropColumn];
|
|
73
|
+
const ifExists = operation.ifExists ? 'IF EXISTS ' : '';
|
|
74
|
+
return columns.map((column) => `DROP COLUMN ${ifExists}${column}`).join(', ');
|
|
75
|
+
}
|
|
76
|
+
return `RENAME COLUMN ${operation.renameColumn.from} TO ${operation.renameColumn.to}`;
|
|
77
|
+
}
|
|
2
78
|
function toUint8Array(data) {
|
|
3
79
|
if (ArrayBuffer.isView(data)) {
|
|
4
80
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
@@ -14,6 +90,7 @@ function normalizeRasterFormat(format) {
|
|
|
14
90
|
}
|
|
15
91
|
export class CereusDB {
|
|
16
92
|
constructor(inner) {
|
|
93
|
+
this.storageBackends = {};
|
|
17
94
|
this.inner = inner;
|
|
18
95
|
}
|
|
19
96
|
/**
|
|
@@ -33,6 +110,17 @@ export class CereusDB {
|
|
|
33
110
|
if (options?.objectStores !== undefined) {
|
|
34
111
|
db.registerObjectStores(options.objectStores);
|
|
35
112
|
}
|
|
113
|
+
db.exportHandler = resolveExportHandler(options?.onExport);
|
|
114
|
+
if (db.exportHandler !== undefined) {
|
|
115
|
+
inner.register_export_handler(db.exportHandler);
|
|
116
|
+
}
|
|
117
|
+
db.storageBackends = resolveStorageBackends(options?.storage);
|
|
118
|
+
for (const [scheme, backend] of Object.entries(db.storageBackends)) {
|
|
119
|
+
inner.register_storage_backend(scheme, backend);
|
|
120
|
+
}
|
|
121
|
+
for (const location of options?.attach ?? []) {
|
|
122
|
+
await db.createDatabase(location, { ifNotExists: true });
|
|
123
|
+
}
|
|
36
124
|
return db;
|
|
37
125
|
}
|
|
38
126
|
/**
|
|
@@ -109,13 +197,140 @@ export class CereusDB {
|
|
|
109
197
|
registerGeoTIFF(name, data) {
|
|
110
198
|
this.registerRaster(name, data, 'geotiff');
|
|
111
199
|
}
|
|
112
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* Drop a table. The table is removed immediately; for tables of persistent
|
|
202
|
+
* databases the returned promise resolves once the change is written.
|
|
203
|
+
*/
|
|
113
204
|
dropTable(name) {
|
|
114
205
|
this.inner.drop_table(name);
|
|
206
|
+
return this.inner.flush();
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Create a persistent database, for example `opfs://mydb`, and attach it
|
|
210
|
+
* as a catalog. Equivalent to `CREATE DATABASE 'opfs://mydb'`.
|
|
211
|
+
* Resolves to the database name.
|
|
212
|
+
*/
|
|
213
|
+
async createDatabase(location, options = {}) {
|
|
214
|
+
return await this.inner.attach_database(location, options.name, true, options.ifNotExists ?? false);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Open an existing persistent database. Equivalent to
|
|
218
|
+
* `ATTACH 'opfs://mydb' [AS name]`. Resolves to the database name.
|
|
219
|
+
*/
|
|
220
|
+
async attachDatabase(location, options = {}) {
|
|
221
|
+
return await this.inner.attach_database(location, options.name, false, options.ifNotExists ?? false);
|
|
222
|
+
}
|
|
223
|
+
/** Write pending changes and close a persistent database. Equivalent to `DETACH name`. */
|
|
224
|
+
async detachDatabase(name, options = {}) {
|
|
225
|
+
await this.inner.detach_database(name, options.ifExists ?? false);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Drop a database by name or location. Persistent databases are deleted
|
|
229
|
+
* from storage. Equivalent to `DROP DATABASE`.
|
|
230
|
+
*/
|
|
231
|
+
async dropDatabase(nameOrLocation, options = {}) {
|
|
232
|
+
await this.inner.drop_database(nameOrLocation, options.ifExists ?? false);
|
|
233
|
+
}
|
|
234
|
+
/** Set the default database (and schema). Equivalent to `USE database[.schema]`. */
|
|
235
|
+
async useDatabase(database, schema) {
|
|
236
|
+
await this.inner.sql_json(schema === undefined ? `USE ${database}` : `USE ${database}.${schema}`);
|
|
237
|
+
}
|
|
238
|
+
/** Attached databases plus stored databases that are not attached. */
|
|
239
|
+
async listDatabases() {
|
|
240
|
+
const rows = (await this.sqlJSON('SELECT database_name, storage, location FROM cereusdb_databases()'));
|
|
241
|
+
const databases = rows.map((row) => ({
|
|
242
|
+
name: row.database_name,
|
|
243
|
+
storage: row.storage,
|
|
244
|
+
location: row.location ?? null,
|
|
245
|
+
attached: true,
|
|
246
|
+
}));
|
|
247
|
+
for (const scheme of Object.keys(this.storageBackends)) {
|
|
248
|
+
const locations = (await this.inner.list_database_locations(scheme));
|
|
249
|
+
for (const location of locations) {
|
|
250
|
+
if (!databases.some((database) => database.location === location)) {
|
|
251
|
+
databases.push({ name: null, storage: scheme, location, attached: false });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return databases;
|
|
256
|
+
}
|
|
257
|
+
/** Describe all databases, schemas, tables and views. */
|
|
258
|
+
async catalog() {
|
|
259
|
+
return JSON.parse(await this.inner.catalog_json());
|
|
115
260
|
}
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Create a table from column definitions or a query. `name` may be
|
|
263
|
+
* qualified (`mydb.public.cities`).
|
|
264
|
+
*/
|
|
265
|
+
async createTable(name, definition, options = {}) {
|
|
266
|
+
const orReplace = options.orReplace ? 'OR REPLACE ' : '';
|
|
267
|
+
const ifNotExists = options.ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
268
|
+
const body = 'as' in definition
|
|
269
|
+
? `AS ${definition.as}`
|
|
270
|
+
: `(${Object.entries(definition.columns)
|
|
271
|
+
.map(([column, type]) => `${column} ${type}`)
|
|
272
|
+
.join(', ')})`;
|
|
273
|
+
await this.inner.sql_json(`CREATE ${orReplace}TABLE ${ifNotExists}${name} ${body}`);
|
|
274
|
+
}
|
|
275
|
+
/** Rename a table or add, drop, or rename a column. */
|
|
276
|
+
async alterTable(name, operation) {
|
|
277
|
+
await this.inner.sql_json(`ALTER TABLE ${name} ${alterTableClause(operation)}`);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Insert Arrow IPC data (stream or file format, for example from
|
|
281
|
+
* `tableToIPC()` of apache-arrow) into a table. Columns are matched by
|
|
282
|
+
* name; missing columns are filled with NULL. Resolves to the number of
|
|
283
|
+
* inserted rows.
|
|
284
|
+
*/
|
|
285
|
+
async insertArrow(table, data) {
|
|
286
|
+
return await this.inner.insert_arrow(table, toUint8Array(data));
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Export a table or query result as a GeoParquet (1.1) file. Geometry and
|
|
290
|
+
* geography columns are described in the file's `geo` metadata.
|
|
291
|
+
*
|
|
292
|
+
* @param queryOrTable A query (`SELECT ...`, `WITH ...`) or a table name.
|
|
293
|
+
*/
|
|
294
|
+
async exportGeoParquet(queryOrTable, options = {}) {
|
|
295
|
+
const query = QUERY_PATTERN.test(queryOrTable)
|
|
296
|
+
? queryOrTable
|
|
297
|
+
: `SELECT * FROM ${queryOrTable}`;
|
|
298
|
+
return await this.inner.export_geoparquet(query, options);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Export a table or query result as GeoParquet and pass it to the export
|
|
302
|
+
* handler: a browser download by default (see `onExport` in
|
|
303
|
+
* {@link CereusDBOptions}). Resolves to the file name.
|
|
304
|
+
*
|
|
305
|
+
* @param queryOrTable A query (`SELECT ...`, `WITH ...`) or a table name.
|
|
306
|
+
*/
|
|
307
|
+
async downloadGeoParquet(queryOrTable, options = {}) {
|
|
308
|
+
const { filename, ...exportOptions } = options;
|
|
309
|
+
const handler = this.exportHandler;
|
|
310
|
+
if (handler === undefined) {
|
|
311
|
+
throw new Error('No export handler: downloads need a browser document; pass CereusDB.create({ onExport }) elsewhere');
|
|
312
|
+
}
|
|
313
|
+
const data = await this.exportGeoParquet(queryOrTable, exportOptions);
|
|
314
|
+
const name = filename ?? defaultExportFilename(queryOrTable);
|
|
315
|
+
await handler(name, data, PARQUET_MIME_TYPE);
|
|
316
|
+
return name;
|
|
317
|
+
}
|
|
318
|
+
/** Rewrite all tables of a persistent database into one segment each. */
|
|
319
|
+
async compactDatabase(name) {
|
|
320
|
+
await this.inner.compact_database(name);
|
|
321
|
+
}
|
|
322
|
+
/** Write pending changes of persistent databases (for example after registerGeoJSON()). */
|
|
323
|
+
async flush() {
|
|
324
|
+
await this.inner.flush();
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* List the tables and views of all databases. By default only the table
|
|
328
|
+
* names are returned, which is ambiguous when databases contain tables with
|
|
329
|
+
* the same name; pass `{ qualified: true }` for `database.schema.table`
|
|
330
|
+
* names, or use {@link CereusDB.catalog} for full details.
|
|
331
|
+
*/
|
|
332
|
+
tables(options = {}) {
|
|
333
|
+
return options.qualified ? this.inner.qualified_tables() : this.inner.tables();
|
|
119
334
|
}
|
|
120
335
|
/** Version string. */
|
|
121
336
|
version() {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage backends for persistent databases (`CREATE DATABASE 'opfs://mydb'`).
|
|
3
|
+
*
|
|
4
|
+
* A backend stores the files of persistent databases. Paths are relative to
|
|
5
|
+
* the backend root and use `/` as separator, for example
|
|
6
|
+
* `mydb/manifest.json` or `mydb/segments/1.arrow`.
|
|
7
|
+
*/
|
|
8
|
+
export interface StorageBackend {
|
|
9
|
+
/** Read a file. Resolves to `null` if it does not exist. */
|
|
10
|
+
readFile(path: string): Promise<Uint8Array | null>;
|
|
11
|
+
/** Create or replace a file. Should replace the file atomically. */
|
|
12
|
+
writeFile(path: string, data: Uint8Array): Promise<void>;
|
|
13
|
+
/** Remove a file or directory (recursively). Missing entries are ignored. */
|
|
14
|
+
remove(path: string): Promise<void>;
|
|
15
|
+
/** List entry names of a directory; directory names end with `/`. Missing directories list as empty. */
|
|
16
|
+
list(path: string): Promise<string[]>;
|
|
17
|
+
/** Acquire an exclusive lock (for example across browser tabs). Rejects if it is held elsewhere. */
|
|
18
|
+
lock?(name: string): Promise<void>;
|
|
19
|
+
/** Release a lock acquired with `lock()`. */
|
|
20
|
+
unlock?(name: string): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Keeps files in memory. Useful for tests, Node.js, and environments without
|
|
24
|
+
* OPFS. Share one instance between CereusDB instances to simulate a reload.
|
|
25
|
+
*/
|
|
26
|
+
export declare class MemoryStorageBackend implements StorageBackend {
|
|
27
|
+
private readonly files;
|
|
28
|
+
private readonly locks;
|
|
29
|
+
readFile(path: string): Promise<Uint8Array | null>;
|
|
30
|
+
writeFile(path: string, data: Uint8Array): Promise<void>;
|
|
31
|
+
remove(path: string): Promise<void>;
|
|
32
|
+
list(path: string): Promise<string[]>;
|
|
33
|
+
lock(name: string): Promise<void>;
|
|
34
|
+
unlock(name: string): Promise<void>;
|
|
35
|
+
/** All stored file paths (for inspection and tests). */
|
|
36
|
+
paths(): string[];
|
|
37
|
+
}
|
|
38
|
+
export interface OPFSStorageBackendOptions {
|
|
39
|
+
/** Directory inside the origin private file system. Defaults to `cereusdb`. */
|
|
40
|
+
directory?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Stores databases in the Origin Private File System (OPFS). Used for
|
|
44
|
+
* `opfs://` locations by default when the browser supports OPFS.
|
|
45
|
+
*
|
|
46
|
+
* Opening a database takes a Web Lock, so a database can be open in only one
|
|
47
|
+
* tab or worker at a time.
|
|
48
|
+
*/
|
|
49
|
+
export declare class OPFSStorageBackend implements StorageBackend {
|
|
50
|
+
private readonly directory;
|
|
51
|
+
private rootPromise?;
|
|
52
|
+
private readonly releases;
|
|
53
|
+
constructor(options?: OPFSStorageBackendOptions);
|
|
54
|
+
/** Whether OPFS is available in the current environment. */
|
|
55
|
+
static isSupported(): boolean;
|
|
56
|
+
private root;
|
|
57
|
+
private directoryHandle;
|
|
58
|
+
readFile(path: string): Promise<Uint8Array | null>;
|
|
59
|
+
writeFile(path: string, data: Uint8Array): Promise<void>;
|
|
60
|
+
remove(path: string): Promise<void>;
|
|
61
|
+
list(path: string): Promise<string[]>;
|
|
62
|
+
lock(name: string): Promise<void>;
|
|
63
|
+
unlock(name: string): Promise<void>;
|
|
64
|
+
}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
function splitPath(path) {
|
|
2
|
+
return path.split('/').filter((segment) => segment.length > 0);
|
|
3
|
+
}
|
|
4
|
+
function isNotFound(error) {
|
|
5
|
+
const name = error?.name;
|
|
6
|
+
return name === 'NotFoundError' || name === 'TypeMismatchError';
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Keeps files in memory. Useful for tests, Node.js, and environments without
|
|
10
|
+
* OPFS. Share one instance between CereusDB instances to simulate a reload.
|
|
11
|
+
*/
|
|
12
|
+
export class MemoryStorageBackend {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.files = new Map();
|
|
15
|
+
this.locks = new Set();
|
|
16
|
+
}
|
|
17
|
+
async readFile(path) {
|
|
18
|
+
const data = this.files.get(splitPath(path).join('/'));
|
|
19
|
+
return data ? data.slice() : null;
|
|
20
|
+
}
|
|
21
|
+
async writeFile(path, data) {
|
|
22
|
+
this.files.set(splitPath(path).join('/'), data.slice());
|
|
23
|
+
}
|
|
24
|
+
async remove(path) {
|
|
25
|
+
const key = splitPath(path).join('/');
|
|
26
|
+
for (const existing of [...this.files.keys()]) {
|
|
27
|
+
if (key === '' || existing === key || existing.startsWith(`${key}/`)) {
|
|
28
|
+
this.files.delete(existing);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async list(path) {
|
|
33
|
+
const prefix = splitPath(path).join('/');
|
|
34
|
+
const entries = new Set();
|
|
35
|
+
for (const key of this.files.keys()) {
|
|
36
|
+
if (prefix !== '' && !key.startsWith(`${prefix}/`)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const rest = prefix === '' ? key : key.slice(prefix.length + 1);
|
|
40
|
+
const [head, ...tail] = rest.split('/');
|
|
41
|
+
entries.add(tail.length > 0 ? `${head}/` : head);
|
|
42
|
+
}
|
|
43
|
+
return [...entries].sort();
|
|
44
|
+
}
|
|
45
|
+
async lock(name) {
|
|
46
|
+
if (this.locks.has(name)) {
|
|
47
|
+
throw new Error('it is already open in another CereusDB instance');
|
|
48
|
+
}
|
|
49
|
+
this.locks.add(name);
|
|
50
|
+
}
|
|
51
|
+
async unlock(name) {
|
|
52
|
+
this.locks.delete(name);
|
|
53
|
+
}
|
|
54
|
+
/** All stored file paths (for inspection and tests). */
|
|
55
|
+
paths() {
|
|
56
|
+
return [...this.files.keys()].sort();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Stores databases in the Origin Private File System (OPFS). Used for
|
|
61
|
+
* `opfs://` locations by default when the browser supports OPFS.
|
|
62
|
+
*
|
|
63
|
+
* Opening a database takes a Web Lock, so a database can be open in only one
|
|
64
|
+
* tab or worker at a time.
|
|
65
|
+
*/
|
|
66
|
+
export class OPFSStorageBackend {
|
|
67
|
+
constructor(options = {}) {
|
|
68
|
+
this.releases = new Map();
|
|
69
|
+
this.directory = options.directory ?? 'cereusdb';
|
|
70
|
+
}
|
|
71
|
+
/** Whether OPFS is available in the current environment. */
|
|
72
|
+
static isSupported() {
|
|
73
|
+
return (typeof navigator !== 'undefined' &&
|
|
74
|
+
typeof navigator.storage?.getDirectory === 'function');
|
|
75
|
+
}
|
|
76
|
+
root() {
|
|
77
|
+
this.rootPromise ?? (this.rootPromise = navigator.storage
|
|
78
|
+
.getDirectory()
|
|
79
|
+
.then((root) => root.getDirectoryHandle(this.directory, { create: true })));
|
|
80
|
+
return this.rootPromise;
|
|
81
|
+
}
|
|
82
|
+
async directoryHandle(segments, create) {
|
|
83
|
+
let directory = await this.root();
|
|
84
|
+
for (const segment of segments) {
|
|
85
|
+
try {
|
|
86
|
+
directory = await directory.getDirectoryHandle(segment, { create });
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (isNotFound(error)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return directory;
|
|
96
|
+
}
|
|
97
|
+
async readFile(path) {
|
|
98
|
+
const segments = splitPath(path);
|
|
99
|
+
const name = segments.pop();
|
|
100
|
+
if (name === undefined) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const directory = await this.directoryHandle(segments, false);
|
|
104
|
+
if (directory === null) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const file = await (await directory.getFileHandle(name)).getFile();
|
|
109
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (isNotFound(error)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async writeFile(path, data) {
|
|
119
|
+
const segments = splitPath(path);
|
|
120
|
+
const name = segments.pop();
|
|
121
|
+
if (name === undefined) {
|
|
122
|
+
throw new Error('writeFile requires a file path');
|
|
123
|
+
}
|
|
124
|
+
const directory = await this.directoryHandle(segments, true);
|
|
125
|
+
if (directory === null) {
|
|
126
|
+
throw new Error(`cannot create directory for ${path}`);
|
|
127
|
+
}
|
|
128
|
+
const handle = await directory.getFileHandle(name, { create: true });
|
|
129
|
+
if (typeof handle.createWritable === 'function') {
|
|
130
|
+
// Writes go to a swap file that replaces the original on close().
|
|
131
|
+
const writable = await handle.createWritable();
|
|
132
|
+
try {
|
|
133
|
+
await writable.write(data);
|
|
134
|
+
await writable.close();
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
await writable.abort().catch(() => undefined);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Older Safari only supports synchronous access handles (in workers).
|
|
143
|
+
const createSyncAccessHandle = handle.createSyncAccessHandle;
|
|
144
|
+
if (typeof createSyncAccessHandle !== 'function') {
|
|
145
|
+
throw new Error('this browser cannot write OPFS files outside a worker');
|
|
146
|
+
}
|
|
147
|
+
const access = await createSyncAccessHandle.call(handle);
|
|
148
|
+
try {
|
|
149
|
+
access.truncate(0);
|
|
150
|
+
access.write(data, { at: 0 });
|
|
151
|
+
access.flush();
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
access.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async remove(path) {
|
|
158
|
+
const segments = splitPath(path);
|
|
159
|
+
const name = segments.pop();
|
|
160
|
+
if (name === undefined) {
|
|
161
|
+
throw new Error('remove requires a path');
|
|
162
|
+
}
|
|
163
|
+
const directory = await this.directoryHandle(segments, false);
|
|
164
|
+
if (directory === null) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
await directory.removeEntry(name, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (!isNotFound(error)) {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async list(path) {
|
|
177
|
+
const directory = await this.directoryHandle(splitPath(path), false);
|
|
178
|
+
if (directory === null) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
const entries = [];
|
|
182
|
+
const iterable = directory;
|
|
183
|
+
for await (const [name, handle] of iterable.entries()) {
|
|
184
|
+
entries.push(handle.kind === 'directory' ? `${name}/` : name);
|
|
185
|
+
}
|
|
186
|
+
return entries.sort();
|
|
187
|
+
}
|
|
188
|
+
async lock(name) {
|
|
189
|
+
if (typeof navigator === 'undefined' || navigator.locks === undefined) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
await new Promise((resolve, reject) => {
|
|
193
|
+
navigator.locks
|
|
194
|
+
.request(name, { ifAvailable: true }, (lock) => {
|
|
195
|
+
if (lock === null) {
|
|
196
|
+
reject(new Error('it is open in another tab or worker'));
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
resolve();
|
|
200
|
+
// Hold the lock until unlock() resolves this promise.
|
|
201
|
+
return new Promise((release) => this.releases.set(name, release));
|
|
202
|
+
})
|
|
203
|
+
.catch(reject);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
async unlock(name) {
|
|
207
|
+
const release = this.releases.get(name);
|
|
208
|
+
this.releases.delete(name);
|
|
209
|
+
release?.();
|
|
210
|
+
}
|
|
211
|
+
}
|