Snapshot via local temp + plain copy — CIFS can't host VACUUM INTO
The first prod snapshot landed as a 0-byte file with a stale journal: SQLite needs byte-range locking on the VACUUM INTO destination, which the CIFS storage box refuses. Now: vacuum into a local temp file, then a plain sequential copy to the archive (always CIFS-safe), cleaning up the temp and any leftover journal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+11
-6
@@ -1,5 +1,5 @@
|
|||||||
import { DatabaseSync } from 'node:sqlite';
|
import { DatabaseSync } from 'node:sqlite';
|
||||||
import { mkdirSync, readdirSync, rmSync } from 'node:fs';
|
import { copyFileSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
// Persistence via Node's built-in SQLite (no native deps). One file on a Docker
|
// Persistence via Node's built-in SQLite (no native deps). One file on a Docker
|
||||||
@@ -135,17 +135,21 @@ export function cacheSet(key: string, value: string, ttl: number): void {
|
|||||||
.run(key, value, Date.now(), ttl);
|
.run(key, value, Date.now(), ttl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Nightly consistent snapshot via VACUUM INTO (transaction-safe, works on any
|
/** Nightly consistent snapshot: VACUUM INTO a LOCAL temp file (SQLite needs
|
||||||
* filesystem — the archive dir may be a CIFS-mounted storage box, where the
|
* real locking, which the CIFS-mounted archive cannot offer), then a plain
|
||||||
* LIVE database could never run). Keeps the newest `keep` dated copies. */
|
* sequential copy into the archive dir. Keeps the newest `keep` dated copies. */
|
||||||
export function backupDb(keep = 14): string | null {
|
export function backupDb(keep = 14): string | null {
|
||||||
const dir = process.env.ARCHIVE_DIR ?? path.join(DATA_DIR, 'archive');
|
const dir = process.env.ARCHIVE_DIR ?? path.join(DATA_DIR, 'archive');
|
||||||
|
const tmp = path.join(DATA_DIR, 'backup-tmp.db');
|
||||||
try {
|
try {
|
||||||
mkdirSync(dir, { recursive: true });
|
mkdirSync(dir, { recursive: true });
|
||||||
|
rmSync(tmp, { force: true }); // VACUUM INTO refuses to overwrite
|
||||||
|
db().exec(`VACUUM INTO '${tmp.replaceAll("'", "''")}'`);
|
||||||
const stamp = new Date().toISOString().slice(0, 10);
|
const stamp = new Date().toISOString().slice(0, 10);
|
||||||
const dest = path.join(dir, `cup26-${stamp}.db`);
|
const dest = path.join(dir, `cup26-${stamp}.db`);
|
||||||
rmSync(dest, { force: true }); // VACUUM INTO refuses to overwrite
|
copyFileSync(tmp, dest);
|
||||||
db().exec(`VACUUM INTO '${dest.replaceAll("'", "''")}'`);
|
rmSync(tmp, { force: true });
|
||||||
|
rmSync(`${dest}-journal`, { force: true }); // leftover from any failed run
|
||||||
const files = readdirSync(dir).filter((f) => /^cup26-\d{4}-\d{2}-\d{2}\.db$/.test(f)).sort();
|
const files = readdirSync(dir).filter((f) => /^cup26-\d{4}-\d{2}-\d{2}\.db$/.test(f)).sort();
|
||||||
for (const f of files.slice(0, Math.max(0, files.length - keep))) {
|
for (const f of files.slice(0, Math.max(0, files.length - keep))) {
|
||||||
rmSync(path.join(dir, f), { force: true });
|
rmSync(path.join(dir, f), { force: true });
|
||||||
@@ -153,6 +157,7 @@ export function backupDb(keep = 14): string | null {
|
|||||||
console.log(`[db] snapshot → ${dest}`);
|
console.log(`[db] snapshot → ${dest}`);
|
||||||
return dest;
|
return dest;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
rmSync(tmp, { force: true });
|
||||||
console.error('[db] backup failed:', e instanceof Error ? e.message : e);
|
console.error('[db] backup failed:', e instanceof Error ? e.message : e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user