From cb12824da40fdc1948da3ad23b57e160b6ae95a0 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Mon, 11 Aug 2025 23:04:26 -0700 Subject: [PATCH] feat: add sqlite database reloading this is used by scan3 to notify active servers that there is new data to be aware of. --- framework/lib/sqlite.ts | 40 ++++++++++++++++++++++++++---------- src/file-viewer/bin/scan3.ts | 27 +++++++++++++++++++++++- src/source-of-truth.ts | 13 ++++++++++-- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/framework/lib/sqlite.ts b/framework/lib/sqlite.ts index 782f003..477ab9e 100644 --- a/framework/lib/sqlite.ts +++ b/framework/lib/sqlite.ts @@ -1,29 +1,32 @@ // Guard against reloads and bundler duplication. +type DbMap = Map; // @ts-ignore -const map = globalThis[Symbol.for("clover.db")] ??= new Map< - string, - WrappedDatabase ->(); +const map = (globalThis[Symbol.for("clover.db")] as DbMap) ??= new Map(); +for (const v of map.values()) { + v.node.close(); +} +map.clear(); export function getDb(file: string) { - let db: WrappedDatabase | null = map.get(file); + let db = map.get(file); if (db) return db; const fileWithExt = file.includes(".") ? file : file + ".sqlite"; db = new WrappedDatabase( - new DatabaseSync( - path.join(process.env.CLOVER_DB ?? ".clover", fileWithExt), - ), + path.join(process.env.CLOVER_DB ?? ".clover", fileWithExt), ); map.set(file, db); return db; } export class WrappedDatabase { + file: string; node: DatabaseSync; + stmts: Stmt[]; stmtTableMigrate: WeakRef | null = null; - constructor(node: DatabaseSync) { - this.node = node; + constructor(file: string) { + this.file = file; + this.node = new DatabaseSync(file); this.node.exec(` create table if not exists clover_migrations ( key text not null primary key, @@ -66,7 +69,18 @@ export class WrappedDatabase { if (err) (err as { query: string }).query = query; throw err; } - return new Stmt(prepared); + const stmt = new Stmt(prepared); + this.stmts.push(stmt); + return stmt; + } + + reload() { + const newNode = new DatabaseSync(this.file); + this.node.close(); + this.node = newNode; + for (const stmt of this.stmts) { + stmt["reload"](newNode); + } } } @@ -80,6 +94,10 @@ export class Stmt { this.query = node.sourceSQL; } + private reload(db: DatabaseSync) { + this.#node = db.prepare(this.query); + } + /** Get one row */ get(...args: Args): Row | null { return this.#wrap(args, () => { diff --git a/src/file-viewer/bin/scan3.ts b/src/file-viewer/bin/scan3.ts index c824eb5..261100e 100644 --- a/src/file-viewer/bin/scan3.ts +++ b/src/file-viewer/bin/scan3.ts @@ -12,6 +12,7 @@ // quality cannot be written with AI. const root = path.resolve("/Volumes/clover/Published"); const workDir = path.resolve(".clover/derived"); +const sotToken = UNWRAP(process.env.CLOVER_SOT_KEY); export async function main() { const start = performance.now(); @@ -337,13 +338,37 @@ export async function main() { console.info("No new derived assets"); } + MediaFile.db.prepare("VACUUM").run(); + MediaFile.db.reload(); + // TODO: reload prod web instance + await rsync.spawn({ + args: [ + MediaFile.db.file, + "clo@zenith:/mnt/storage1/clover/Documents/Config/paperclover/cache.sqlite", + ], + title: "Uploading Database", + cwd: process.cwd(), + }); + { + const res = await fetch("https://db.paperclover.net/reload", { + method: "post", + headers: { + Authorization: sotToken, + }, + }); + if (!res.ok) { + console.warn( + `Failed to reload remote database ${res.status} ${res.statusText}`, + ); + } + } + console.info( "Updated file viewer index in \x1b[1m" + ((performance.now() - start) / 1000).toFixed(1) + "s\x1b[0m", ); - MediaFile.db.prepare("VACUUM").run(); const { duration, count } = MediaFile.db .prepare<[], { count: number; duration: number }>( ` diff --git a/src/source-of-truth.ts b/src/source-of-truth.ts index 19cf009..af2826f 100644 --- a/src/source-of-truth.ts +++ b/src/source-of-truth.ts @@ -20,7 +20,7 @@ const app = new Hono(); export default app; -const token = process.env.CLOVER_SOT_KEY; +const token = UNWRAP(process.env.CLOVER_SOT_KEY); const nasRoot = process.platform === "win32" ? "\\\\zenith\\clover" @@ -48,12 +48,13 @@ const fds = new Map>(); app.get("/file/*", async (c) => { const fullQuery = c.req.path.slice("/file".length); const [filePath, derivedAsset, ...invalid] = fullQuery.split("$/"); + ASSERT(filePath != null); if (invalid.length > 0) return c.notFound(); if (filePath.length <= 1) return c.notFound(); const permissions = FilePermissions.getByPrefix(filePath); if (permissions !== 0) { if (c.req.header("Authorization") !== token) { - return c.json({ error: "invalid authorization header" }); + return c.json({ error: "invalid authorization header" }, 401); } } const file = MediaFile.getByPath(filePath); @@ -104,6 +105,14 @@ app.get("/file/*", async (c) => { return c.body(stream.Readable.toWeb(nodeStream) as ReadableStream); }); +app.post("/reload", async (c) => { + if (c.req.header("Authorization") !== token) { + return c.json({ error: "invalid authorization header" }, 401); + } + MediaFile.db.reload(); + return c.body(null, 204); +}); + const openFile = util.promisify(fsCallbacks.open); const closeFile = util.promisify(fsCallbacks.close);