| author | |
| committer | |
| log | bc5017b0e58626345037c3bd2e32e6a2a1b576b4 |
| tree | 9e2a50c394352f533ee2dc8f3790836675a700ab |
| parent | 98be3e2bd40a40c6595de522d087092df63cdbb7 |
| signature | Commit is signed but in an unrecognized format. |
this is used by scan3 to notify active servers that there is new data to
be aware of.3 files changed, 66 insertions(+), 14 deletions(-)
framework/lib/sqlite.ts+29-11| ... | @@ -1,29 +1,32 @@ | ... | @@ -1,29 +1,32 @@ |
| 1 | // Guard against reloads and bundler duplication. | 1 | // Guard against reloads and bundler duplication. |
| 2 | type DbMap = Map<string, WrappedDatabase>; | ||
| 2 | // @ts-ignore | 3 | // @ts-ignore |
| 3 | const map = globalThis[Symbol.for("clover.db")] ??= new Map< | 4 | const map = (globalThis[Symbol.for("clover.db")] as DbMap) ??= new Map(); |
| 4 | string, | 5 | for (const v of map.values()) { |
| 5 | WrappedDatabase | 6 | v.node.close(); |
| 6 | >(); | 7 | } |
| 8 | map.clear(); | ||
| 7 | 9 | ||
| 8 | export function getDb(file: string) { | 10 | export function getDb(file: string) { |
| 9 | let db: WrappedDatabase | null = map.get(file); | 11 | let db = map.get(file); |
| 10 | if (db) return db; | 12 | if (db) return db; |
| 11 | const fileWithExt = file.includes(".") ? file : file + ".sqlite"; | 13 | const fileWithExt = file.includes(".") ? file : file + ".sqlite"; |
| 12 | db = new WrappedDatabase( | 14 | db = new WrappedDatabase( |
| 13 | new DatabaseSync( | 15 | path.join(process.env.CLOVER_DB ?? ".clover", fileWithExt), |
| 14 | path.join(process.env.CLOVER_DB ?? ".clover", fileWithExt), | ||
| 15 | ), | ||
| 16 | ); | 16 | ); |
| 17 | map.set(file, db); | 17 | map.set(file, db); |
| 18 | return db; | 18 | return db; |
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | export class WrappedDatabase { | 21 | export class WrappedDatabase { |
| 22 | file: string; | ||
| 22 | node: DatabaseSync; | 23 | node: DatabaseSync; |
| 24 | stmts: Stmt[]; | ||
| 23 | stmtTableMigrate: WeakRef<StatementSync> | null = null; | 25 | stmtTableMigrate: WeakRef<StatementSync> | null = null; |
| 24 | 26 | ||
| 25 | constructor(node: DatabaseSync) { | 27 | constructor(file: string) { |
| 26 | this.node = node; | 28 | this.file = file; |
| 29 | this.node = new DatabaseSync(file); | ||
| 27 | this.node.exec(` | 30 | this.node.exec(` |
| 28 | create table if not exists clover_migrations ( | 31 | create table if not exists clover_migrations ( |
| 29 | key text not null primary key, | 32 | key text not null primary key, |
| ... | @@ -66,7 +69,18 @@ export class WrappedDatabase { | ... | @@ -66,7 +69,18 @@ export class WrappedDatabase { |
| 66 | if (err) (err as { query: string }).query = query; | 69 | if (err) (err as { query: string }).query = query; |
| 67 | throw err; | 70 | throw err; |
| 68 | } | 71 | } |
| 69 | return new Stmt(prepared); | 72 | const stmt = new Stmt<Args, Result>(prepared); |
| 73 | this.stmts.push(stmt); | ||
| 74 | return stmt; | ||
| 75 | } | ||
| 76 | |||
| 77 | reload() { | ||
| 78 | const newNode = new DatabaseSync(this.file); | ||
| 79 | this.node.close(); | ||
| 80 | this.node = newNode; | ||
| 81 | for (const stmt of this.stmts) { | ||
| 82 | stmt["reload"](newNode); | ||
| 83 | } | ||
| 70 | } | 84 | } |
| 71 | } | 85 | } |
| 72 | 86 | ||
| ... | @@ -80,6 +94,10 @@ export class Stmt<Args extends unknown[] = unknown[], Row = unknown> { | ... | @@ -80,6 +94,10 @@ export class Stmt<Args extends unknown[] = unknown[], Row = unknown> { |
| 80 | this.query = node.sourceSQL; | 94 | this.query = node.sourceSQL; |
| 81 | } | 95 | } |
| 82 | 96 | ||
| 97 | private reload(db: DatabaseSync) { | ||
| 98 | this.#node = db.prepare(this.query); | ||
| 99 | } | ||
| 100 | |||
| 83 | /** Get one row */ | 101 | /** Get one row */ |
| 84 | get(...args: Args): Row | null { | 102 | get(...args: Args): Row | null { |
| 85 | return this.#wrap(args, () => { | 103 | return this.#wrap(args, () => { |
src/file-viewer/bin/scan3.ts+26-1| ... | @@ -12,6 +12,7 @@ | ... | @@ -12,6 +12,7 @@ |
| 12 | // quality cannot be written with AI. | 12 | // quality cannot be written with AI. |
| 13 | const root = path.resolve("/Volumes/clover/Published"); | 13 | const root = path.resolve("/Volumes/clover/Published"); |
| 14 | const workDir = path.resolve(".clover/derived"); | 14 | const workDir = path.resolve(".clover/derived"); |
| 15 | const sotToken = UNWRAP(process.env.CLOVER_SOT_KEY); | ||
| 15 | 16 | ||
| 16 | export async function main() { | 17 | export async function main() { |
| 17 | const start = performance.now(); | 18 | const start = performance.now(); |
| ... | @@ -337,13 +338,37 @@ export async function main() { | ... | @@ -337,13 +338,37 @@ export async function main() { |
| 337 | console.info("No new derived assets"); | 338 | console.info("No new derived assets"); |
| 338 | } | 339 | } |
| 339 | 340 | ||
| 341 | MediaFile.db.prepare("VACUUM").run(); | ||
| 342 | MediaFile.db.reload(); | ||
| 343 | // TODO: reload prod web instance | ||
| 344 | await rsync.spawn({ | ||
| 345 | args: [ | ||
| 346 | MediaFile.db.file, | ||
| 347 | "clo@zenith:/mnt/storage1/clover/Documents/Config/paperclover/cache.sqlite", | ||
| 348 | ], | ||
| 349 | title: "Uploading Database", | ||
| 350 | cwd: process.cwd(), | ||
| 351 | }); | ||
| 352 | { | ||
| 353 | const res = await fetch("https://db.paperclover.net/reload", { | ||
| 354 | method: "post", | ||
| 355 | headers: { | ||
| 356 | Authorization: sotToken, | ||
| 357 | }, | ||
| 358 | }); | ||
| 359 | if (!res.ok) { | ||
| 360 | console.warn( | ||
| 361 | `Failed to reload remote database ${res.status} ${res.statusText}`, | ||
| 362 | ); | ||
| 363 | } | ||
| 364 | } | ||
| 365 | |||
| 340 | console.info( | 366 | console.info( |
| 341 | "Updated file viewer index in \x1b[1m" + | 367 | "Updated file viewer index in \x1b[1m" + |
| 342 | ((performance.now() - start) / 1000).toFixed(1) + | 368 | ((performance.now() - start) / 1000).toFixed(1) + |
| 343 | "s\x1b[0m", | 369 | "s\x1b[0m", |
| 344 | ); | 370 | ); |
| 345 | 371 | ||
| 346 | MediaFile.db.prepare("VACUUM").run(); | ||
| 347 | const { duration, count } = MediaFile.db | 372 | const { duration, count } = MediaFile.db |
| 348 | .prepare<[], { count: number; duration: number }>( | 373 | .prepare<[], { count: number; duration: number }>( |
| 349 | ` | 374 | ` |
src/source-of-truth.ts+11-2| ... | @@ -20,7 +20,7 @@ | ... | @@ -20,7 +20,7 @@ |
| 20 | const app = new Hono(); | 20 | const app = new Hono(); |
| 21 | export default app; | 21 | export default app; |
| 22 | 22 | ||
| 23 | const token = process.env.CLOVER_SOT_KEY; | 23 | const token = UNWRAP(process.env.CLOVER_SOT_KEY); |
| 24 | 24 | ||
| 25 | const nasRoot = process.platform === "win32" | 25 | const nasRoot = process.platform === "win32" |
| 26 | ? "\\\\zenith\\clover" | 26 | ? "\\\\zenith\\clover" |
| ... | @@ -48,12 +48,13 @@ const fds = new Map<string, Awaitable<{ fd: number; refs: number }>>(); | ... | @@ -48,12 +48,13 @@ const fds = new Map<string, Awaitable<{ fd: number; refs: number }>>(); |
| 48 | app.get("/file/*", async (c) => { | 48 | app.get("/file/*", async (c) => { |
| 49 | const fullQuery = c.req.path.slice("/file".length); | 49 | const fullQuery = c.req.path.slice("/file".length); |
| 50 | const [filePath, derivedAsset, ...invalid] = fullQuery.split("$/"); | 50 | const [filePath, derivedAsset, ...invalid] = fullQuery.split("$/"); |
| 51 | ASSERT(filePath != null); | ||
| 51 | if (invalid.length > 0) return c.notFound(); | 52 | if (invalid.length > 0) return c.notFound(); |
| 52 | if (filePath.length <= 1) return c.notFound(); | 53 | if (filePath.length <= 1) return c.notFound(); |
| 53 | const permissions = FilePermissions.getByPrefix(filePath); | 54 | const permissions = FilePermissions.getByPrefix(filePath); |
| 54 | if (permissions !== 0) { | 55 | if (permissions !== 0) { |
| 55 | if (c.req.header("Authorization") !== token) { | 56 | if (c.req.header("Authorization") !== token) { |
| 56 | return c.json({ error: "invalid authorization header" }); | 57 | return c.json({ error: "invalid authorization header" }, 401); |
| 57 | } | 58 | } |
| 58 | } | 59 | } |
| 59 | const file = MediaFile.getByPath(filePath); | 60 | const file = MediaFile.getByPath(filePath); |
| ... | @@ -104,6 +105,14 @@ app.get("/file/*", async (c) => { | ... | @@ -104,6 +105,14 @@ app.get("/file/*", async (c) => { |
| 104 | return c.body(stream.Readable.toWeb(nodeStream) as ReadableStream); | 105 | return c.body(stream.Readable.toWeb(nodeStream) as ReadableStream); |
| 105 | }); | 106 | }); |
| 106 | 107 | ||
| 108 | app.post("/reload", async (c) => { | ||
| 109 | if (c.req.header("Authorization") !== token) { | ||
| 110 | return c.json({ error: "invalid authorization header" }, 401); | ||
| 111 | } | ||
| 112 | MediaFile.db.reload(); | ||
| 113 | return c.body(null, 204); | ||
| 114 | }); | ||
| 115 | |||
| 107 | const openFile = util.promisify(fsCallbacks.open); | 116 | const openFile = util.promisify(fsCallbacks.open); |
| 108 | const closeFile = util.promisify(fsCallbacks.close); | 117 | const closeFile = util.promisify(fsCallbacks.close); |
| 109 | 118 |