From 5c2cb331c8846e4d31983ff7e6516a0156214d33 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Mon, 18 Dec 2023 15:11:20 +0500 Subject: [PATCH] web: add support for exporting sql database --- apps/web/src/common/sqlite/sqlite-export.ts | 101 ++++++++++++++++++++ apps/web/src/common/sqlite/sqlite.kysely.ts | 4 + apps/web/src/common/sqlite/sqlite.worker.ts | 19 +++- 3 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/common/sqlite/sqlite-export.ts diff --git a/apps/web/src/common/sqlite/sqlite-export.ts b/apps/web/src/common/sqlite/sqlite-export.ts new file mode 100644 index 000000000..1f0ab0f2d --- /dev/null +++ b/apps/web/src/common/sqlite/sqlite-export.ts @@ -0,0 +1,101 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import * as VFS from "./VFS"; + +export class DatabaseSource { + isDone; + + #vfs; + #path; + #fileId = Math.floor(Math.random() * 0x100000000); + #iOffset = 0; + #bytesRemaining = 0; + + #onDone: (() => Promise | unknown)[] = []; + #resolve!: (value: unknown) => void; + #reject!: (reason?: unknown) => void; + + constructor(vfs: VFS.Base, path: string) { + this.#vfs = vfs; + this.#path = path; + this.isDone = new Promise((resolve, reject) => { + this.#resolve = resolve; + this.#reject = reject; + }).finally(async () => { + while (this.#onDone.length) { + await this.#onDone.pop()?.(); + } + }); + } + + async start(controller: ReadableStreamDefaultController) { + try { + // Open the file for reading. + const flags = VFS.SQLITE_OPEN_MAIN_DB | VFS.SQLITE_OPEN_READONLY; + await check( + this.#vfs.xOpen(this.#path, this.#fileId, flags, { + setInt32() {} + } as unknown as DataView) + ); + this.#onDone.push(() => this.#vfs.xClose(this.#fileId)); + await check(this.#vfs.xLock(this.#fileId, VFS.SQLITE_LOCK_SHARED)); + this.#onDone.push(() => + this.#vfs.xUnlock(this.#fileId, VFS.SQLITE_LOCK_NONE) + ); + + // Get the file size. + const fileSize = new DataView(new ArrayBuffer(8)); + await check(this.#vfs.xFileSize(this.#fileId, fileSize)); + this.#bytesRemaining = Number(fileSize.getBigUint64(0, true)); + } catch (e) { + controller.error(e); + this.#reject(e); + } + } + + async pull(controller: ReadableStreamDefaultController) { + try { + const buffer = new Uint8Array(Math.min(this.#bytesRemaining, 65536)); + await check(this.#vfs.xRead(this.#fileId, buffer, this.#iOffset)); + console.log("reading", buffer); + controller.enqueue(buffer); + + this.#iOffset += buffer.byteLength; + this.#bytesRemaining -= buffer.byteLength; + if (this.#bytesRemaining === 0) { + controller.close(); + this.#resolve(undefined); + } + } catch (e) { + controller.error(e); + this.#reject(e); + } + } + + cancel(reason?: any) { + this.#reject(new Error(reason)); + } +} + +async function check(code: Promise | number) { + if ((await code) !== VFS.SQLITE_OK) { + throw new Error(`Error code: ${await code}`); + } +} diff --git a/apps/web/src/common/sqlite/sqlite.kysely.ts b/apps/web/src/common/sqlite/sqlite.kysely.ts index 075024d3e..29b1517ec 100644 --- a/apps/web/src/common/sqlite/sqlite.kysely.ts +++ b/apps/web/src/common/sqlite/sqlite.kysely.ts @@ -77,6 +77,10 @@ export class WaSqliteWorkerDriver implements Driver { } return await this.worker.close(); } + + async export() { + return this.worker.export(this.config.dbName, this.config.async); + } } class ConnectionMutex { diff --git a/apps/web/src/common/sqlite/sqlite.worker.ts b/apps/web/src/common/sqlite/sqlite.worker.ts index e7f904d26..3a4557d24 100644 --- a/apps/web/src/common/sqlite/sqlite.worker.ts +++ b/apps/web/src/common/sqlite/sqlite.worker.ts @@ -23,9 +23,10 @@ import SQLiteAsyncESMFactory from "./wa-sqlite-async"; import SQLiteSyncESMFactory from "./wa-sqlite"; import { IDBBatchAtomicVFS } from "./IDBBatchAtomicVFS"; import { AccessHandlePoolVFS } from "./AccessHandlePoolVFS"; -import { expose } from "comlink"; +import { expose, transfer } from "comlink"; import type { RunMode } from "./type"; import { QueryResult } from "kysely"; +import { DatabaseSource } from "./sqlite-export"; type PreparedStatement = { stmt: number; @@ -87,8 +88,6 @@ async function run(sql: string, parameters?: SQLiteCompatibleType[]) { } return rows; - } catch (e) { - console.error(e); } finally { await sqlite .reset(prepared.stmt) @@ -101,7 +100,6 @@ async function run(sql: string, parameters?: SQLiteCompatibleType[]) { .finally(() => preparedStatements.delete(sql)) ); } - return []; } async function exec( @@ -130,10 +128,21 @@ async function close() { await vfs?.close(); } +async function exportDatabase(dbName: string, async: boolean) { + const vfs = async + ? new IDBBatchAtomicVFS(dbName, { durability: "strict" }) + : new AccessHandlePoolVFS(dbName); + if ("isReady" in vfs) await vfs.isReady; + + const stream = new ReadableStream(new DatabaseSource(vfs, dbName)); + return transfer(stream, [stream]); +} + const worker = { close, init, - run: exec + run: exec, + export: exportDatabase }; export type SQLiteWorker = typeof worker;