feat: add sqlite database reloading

this is used by scan3 to notify active servers that there is new data to
be aware of.
This commit is contained in:
clover caruso 2025-08-11 23:04:26 -07:00
parent f1d4be2553
commit cb12824da4
3 changed files with 66 additions and 14 deletions

View file

@ -1,29 +1,32 @@
// Guard against reloads and bundler duplication.
type DbMap = Map<string, WrappedDatabase>;
// @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),
),
);
map.set(file, db);
return db;
}
export class WrappedDatabase {
file: string;
node: DatabaseSync;
stmts: Stmt[];
stmtTableMigrate: WeakRef<StatementSync> | 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<Args, Result>(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<Args extends unknown[] = unknown[], Row = unknown> {
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, () => {

View file

@ -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 }>(
`

View file

@ -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<string, Awaitable<{ fd: number; refs: number }>>();
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);