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.
|
||||
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),
|
||||
),
|
||||
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, () => {
|
||||
|
|
|
@ -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 }>(
|
||||
`
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue