127 lines
4.9 KiB
JavaScript
127 lines
4.9 KiB
JavaScript
// Generated, do not edit. sha256:e1360b8b92e9fdeccd0b88044df16a08eafd2bf7c084bf7549eeac1712ff09fb
|
|||
|
|
import { DatabaseSync } from 'node:sqlite';
|
||
|
|
import { writeFile, rename, rm, access } from 'node:fs/promises';
|
||
|
|
import { CONFIG } from '../shared/config.js';
|
||
|
|
import { BASELINE_VERSION } from './db/migrations.js';
|
||
|
|
import { closeDatabase } from './db/conn.mjs';
|
||
|
|
import { initDatabase, dbFile } from './db.js';
|
||
|
|
import { fail } from '../shared/app-error.js';
|
||
|
|
|
||
|
|
const BUREN = ['-wal', '-shm'];
|
||
|
|
|
||
|
|
const VERGRENDELD = new Set(['EBUSY', 'EPERM', 'EACCES']);
|
||
|
|
const WACHT_MS = [50, 150, 400];
|
||
|
|
|
||
|
|
async function hernoemVolhardend(van, naar) {
|
||
|
|
for (let poging = 0; ; poging++) {
|
||
|
|
try { return await rename(van, naar); }
|
||
|
|
catch (e) {
|
||
|
|
if (!VERGRENDELD.has(e.code) || poging >= WACHT_MS.length) throw vertaalVergrendeling(e, van);
|
||
|
|
await new Promise(r => setTimeout(r, WACHT_MS[poging]));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function vertaalVergrendeling(e, pad) {
|
||
|
|
if (!VERGRENDELD.has(e.code)) return e;
|
||
|
|
return new Error(
|
||
|
|
`Het databasebestand is door een ander programma in gebruik (${e.code}), dus het kon niet worden `
|
||
|
|
+ `verplaatst: ${pad}. Er is niets gewijzigd. Sluit wat er nog aan deze datamap hangt en probeer het `
|
||
|
|
+ `opnieuw: een tweede exemplaar van de app, de MCP-server (npm run mcp), of een database-viewer.`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const bestaat = async pad => { try { await access(pad); return true; } catch { return false; } };
|
||
|
|
|
||
|
|
export function keurBestand(pad) {
|
||
|
|
let db;
|
||
|
|
try {
|
||
|
|
db = new DatabaseSync(pad, { readOnly: true });
|
||
|
|
} catch (e) {
|
||
|
|
fail('dbfile.notReadableSqlite', { reden: e.message }, `This is not a readable SQLite file (${e.message}).`);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const heeftSchema = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='Schema'`).get();
|
||
|
|
if (!heeftSchema) {
|
||
|
|
fail('dbfile.noSchemaTable', null,
|
||
|
|
'This file has no Schema table; a database from before the baseline can no longer be converted '
|
||
|
|
+ '(git log --all -- scripts/baseline-convert.mjs).');
|
||
|
|
}
|
||
|
|
const rij = db.prepare(`SELECT version FROM Schema`).get();
|
||
|
|
const versie = rij && Number(rij.version);
|
||
|
|
if (!Number.isInteger(versie)) fail('dbfile.noReadableVersion', null, 'The Schema table holds no readable version.');
|
||
|
|
if (versie < BASELINE_VERSION) {
|
||
|
|
fail('dbfile.versionBeforeBaseline', { versie, baseline: BASELINE_VERSION },
|
||
|
|
`This database is version ${versie}, from before the new layout (version ${BASELINE_VERSION}); `
|
||
|
|
+ 'see git log --all -- scripts/baseline-convert.mjs.');
|
||
|
|
}
|
||
|
|
if (versie > CONFIG.SCHEMA_VERSION) {
|
||
|
|
fail('dbfile.versionNewerThanApp', { versie, app: CONFIG.SCHEMA_VERSION },
|
||
|
|
`This database is version ${versie} and thus newer than the app (${CONFIG.SCHEMA_VERSION}).`);
|
||
|
|
}
|
||
|
|
const wezen = db.prepare(`PRAGMA foreign_key_check`).all();
|
||
|
|
return { versie, wezen: wezen.length };
|
||
|
|
} finally {
|
||
|
|
db.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function restoreDatabase(bytes) {
|
||
|
|
if (!bytes || bytes.length === 0) fail('dbfile.emptyFile', null, 'Received an empty file.');
|
||
|
|
if (bytes.subarray(0, 15).toString('latin1') !== 'SQLite format 3') {
|
||
|
|
fail('dbfile.notSqlite', null, 'This is not a SQLite file (the file header does not match).');
|
||
|
|
}
|
||
|
|
|
||
|
|
const doel = dbFile();
|
||
|
|
const incoming = `${doel}.new`;
|
||
|
|
const previous = `${doel}.previous`;
|
||
|
|
await writeFile(incoming, bytes, { mode: 0o600 });
|
||
|
|
|
||
|
|
let keuring;
|
||
|
|
try {
|
||
|
|
keuring = keurBestand(incoming);
|
||
|
|
} catch (e) {
|
||
|
|
await rm(incoming, { force: true });
|
||
|
|
throw e;
|
||
|
|
}
|
||
|
|
|
||
|
|
await wisselDatabaseBestand(incoming, previous);
|
||
|
|
|
||
|
|
const regel = initDatabase();
|
||
|
|
return {
|
||
|
|
versie: keuring.versie,
|
||
|
|
wezen: keuring.wezen,
|
||
|
|
previous,
|
||
|
|
regel,
|
||
|
|
geheimenWaarschuwing: 'De geheimen (NWC-URI\'s, LND-macaroons, provider-sleutels) staan in '
|
||
|
|
+ 'secrets.json in de map van deze portefeuille en zijn NIET meeverhuisd. Komt deze database uit een andere administratie, '
|
||
|
|
+ 'controleer dan je tracking-bronnen: een geheim hangt aan een bron-id en dat id kan nu bij een '
|
||
|
|
+ 'andere bron horen.',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function wisselDatabaseBestand(incoming, previous, naWissel = async () => {}) {
|
||
|
|
let opzijGezet = false;
|
||
|
|
const doel = dbFile();
|
||
|
|
closeDatabase();
|
||
|
|
try {
|
||
|
|
await rm(previous, { force: true });
|
||
|
|
for (const b of BUREN) await rm(previous + b, { force: true });
|
||
|
|
if (await bestaat(doel)) {
|
||
|
|
await hernoemVolhardend(doel, previous);
|
||
|
|
opzijGezet = true;
|
||
|
|
for (const b of BUREN) if (await bestaat(doel + b)) await hernoemVolhardend(doel + b, previous + b);
|
||
|
|
}
|
||
|
|
await hernoemVolhardend(incoming, doel);
|
||
|
|
await naWissel();
|
||
|
|
} catch (e) {
|
||
|
|
if (opzijGezet && !(await bestaat(doel))) {
|
||
|
|
try {
|
||
|
|
await rename(previous, doel);
|
||
|
|
for (const b of BUREN) if (await bestaat(previous + b)) await rename(previous + b, doel + b);
|
||
|
|
} catch { }
|
||
|
|
}
|
||
|
|
await rm(incoming, { force: true });
|
||
|
|
try { initDatabase(); } catch { }
|
||
|
|
throw e;
|
||
|
|
}
|
||
|
|
}
|