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:
parent
f1d4be2553
commit
cb12824da4
3 changed files with 66 additions and 14 deletions
|
@ -1,29 +1,32 @@
|
||||||
// Guard against reloads and bundler duplication.
|
// Guard against reloads and bundler duplication.
|
||||||
|
type DbMap = Map<string, WrappedDatabase>;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const map = globalThis[Symbol.for("clover.db")] ??= new Map<
|
const map = (globalThis[Symbol.for("clover.db")] as DbMap) ??= new Map();
|
||||||
string,
|
for (const v of map.values()) {
|
||||||
WrappedDatabase
|
v.node.close();
|
||||||
>();
|
}
|
||||||
|
map.clear();
|
||||||
|
|
||||||
export function getDb(file: string) {
|
export function getDb(file: string) {
|
||||||
let db: WrappedDatabase | null = map.get(file);
|
let db = map.get(file);
|
||||||
if (db) return db;
|
if (db) return db;
|
||||||
const fileWithExt = file.includes(".") ? file : file + ".sqlite";
|
const fileWithExt = file.includes(".") ? file : file + ".sqlite";
|
||||||
db = new WrappedDatabase(
|
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);
|
map.set(file, db);
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WrappedDatabase {
|
export class WrappedDatabase {
|
||||||
|
file: string;
|
||||||
node: DatabaseSync;
|
node: DatabaseSync;
|
||||||
|
stmts: Stmt[];
|
||||||
stmtTableMigrate: WeakRef<StatementSync> | null = null;
|
stmtTableMigrate: WeakRef<StatementSync> | null = null;
|
||||||
|
|
||||||
constructor(node: DatabaseSync) {
|
constructor(file: string) {
|
||||||
this.node = node;
|
this.file = file;
|
||||||
|
this.node = new DatabaseSync(file);
|
||||||
this.node.exec(`
|
this.node.exec(`
|
||||||
create table if not exists clover_migrations (
|
create table if not exists clover_migrations (
|
||||||
key text not null primary key,
|
key text not null primary key,
|
||||||
|
@ -66,7 +69,18 @@ export class WrappedDatabase {
|
||||||
if (err) (err as { query: string }).query = query;
|
if (err) (err as { query: string }).query = query;
|
||||||
throw err;
|
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;
|
this.query = node.sourceSQL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private reload(db: DatabaseSync) {
|
||||||
|
this.#node = db.prepare(this.query);
|
||||||
|
}
|
||||||
|
|
||||||
/** Get one row */
|
/** Get one row */
|
||||||
get(...args: Args): Row | null {
|
get(...args: Args): Row | null {
|
||||||
return this.#wrap(args, () => {
|
return this.#wrap(args, () => {
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
// quality cannot be written with AI.
|
// quality cannot be written with AI.
|
||||||
const root = path.resolve("/Volumes/clover/Published");
|
const root = path.resolve("/Volumes/clover/Published");
|
||||||
const workDir = path.resolve(".clover/derived");
|
const workDir = path.resolve(".clover/derived");
|
||||||
|
const sotToken = UNWRAP(process.env.CLOVER_SOT_KEY);
|
||||||
|
|
||||||
export async function main() {
|
export async function main() {
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
|
@ -337,13 +338,37 @@ export async function main() {
|
||||||
console.info("No new derived assets");
|
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(
|
console.info(
|
||||||
"Updated file viewer index in \x1b[1m" +
|
"Updated file viewer index in \x1b[1m" +
|
||||||
((performance.now() - start) / 1000).toFixed(1) +
|
((performance.now() - start) / 1000).toFixed(1) +
|
||||||
"s\x1b[0m",
|
"s\x1b[0m",
|
||||||
);
|
);
|
||||||
|
|
||||||
MediaFile.db.prepare("VACUUM").run();
|
|
||||||
const { duration, count } = MediaFile.db
|
const { duration, count } = MediaFile.db
|
||||||
.prepare<[], { count: number; duration: number }>(
|
.prepare<[], { count: number; duration: number }>(
|
||||||
`
|
`
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
export default app;
|
export default app;
|
||||||
|
|
||||||
const token = process.env.CLOVER_SOT_KEY;
|
const token = UNWRAP(process.env.CLOVER_SOT_KEY);
|
||||||
|
|
||||||
const nasRoot = process.platform === "win32"
|
const nasRoot = process.platform === "win32"
|
||||||
? "\\\\zenith\\clover"
|
? "\\\\zenith\\clover"
|
||||||
|
@ -48,12 +48,13 @@ const fds = new Map<string, Awaitable<{ fd: number; refs: number }>>();
|
||||||
app.get("/file/*", async (c) => {
|
app.get("/file/*", async (c) => {
|
||||||
const fullQuery = c.req.path.slice("/file".length);
|
const fullQuery = c.req.path.slice("/file".length);
|
||||||
const [filePath, derivedAsset, ...invalid] = fullQuery.split("$/");
|
const [filePath, derivedAsset, ...invalid] = fullQuery.split("$/");
|
||||||
|
ASSERT(filePath != null);
|
||||||
if (invalid.length > 0) return c.notFound();
|
if (invalid.length > 0) return c.notFound();
|
||||||
if (filePath.length <= 1) return c.notFound();
|
if (filePath.length <= 1) return c.notFound();
|
||||||
const permissions = FilePermissions.getByPrefix(filePath);
|
const permissions = FilePermissions.getByPrefix(filePath);
|
||||||
if (permissions !== 0) {
|
if (permissions !== 0) {
|
||||||
if (c.req.header("Authorization") !== token) {
|
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);
|
const file = MediaFile.getByPath(filePath);
|
||||||
|
@ -104,6 +105,14 @@ app.get("/file/*", async (c) => {
|
||||||
return c.body(stream.Readable.toWeb(nodeStream) as ReadableStream);
|
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 openFile = util.promisify(fsCallbacks.open);
|
||||||
const closeFile = util.promisify(fsCallbacks.close);
|
const closeFile = util.promisify(fsCallbacks.close);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue