From f3b56fb1cc5a1ae09a69d8951b9c597d641773d4 Mon Sep 17 00:00:00 2001 From: Myx Date: Mon, 13 Apr 2026 02:23:09 +0200 Subject: [PATCH] fix: Db corruption fix --- server/src/db/database.ts | 77 +++++++++++++++++++++++++++++++++++---- server/src/index.ts | 22 ++++++++++- 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/server/src/db/database.ts b/server/src/db/database.ts index d612659..8542685 100644 --- a/server/src/db/database.ts +++ b/server/src/db/database.ts @@ -17,11 +17,77 @@ import { import { serverMigrations } from '../migrations'; import { findExistingPath, resolveRuntimePath } from '../runtime-paths'; -const DATA_DIR = resolveRuntimePath('data'); -const DB_FILE = path.join(DATA_DIR, 'metoyou.sqlite'); +function resolveDbFile(): string { + const envPath = process.env.DB_PATH; + + if (envPath) { + return path.resolve(envPath); + } + + return path.join(resolveRuntimePath('data'), 'metoyou.sqlite'); +} + +const DB_FILE = resolveDbFile(); +const DB_BACKUP = DB_FILE + '.bak'; +const DATA_DIR = path.dirname(DB_FILE); +// SQLite files start with this 16-byte header string. +const SQLITE_MAGIC = 'SQLite format 3\0'; let applicationDataSource: DataSource | undefined; +/** + * Returns true when `data` looks like a valid SQLite file + * (correct header magic and at least one complete page). + */ +function isValidSqlite(data: Uint8Array): boolean { + if (data.length < 100) + return false; + + const header = Buffer.from(data.buffer, data.byteOffset, 16).toString('ascii'); + + return header === SQLITE_MAGIC; +} + +/** + * Back up the current DB file so there is always a recovery point. + * If the main file is corrupted/empty but a valid backup exists, + * restore the backup before the server loads the database. + */ +function safeguardDbFile(): Uint8Array | undefined { + if (!fs.existsSync(DB_FILE)) + return undefined; + + const data = new Uint8Array(fs.readFileSync(DB_FILE)); + + if (isValidSqlite(data)) { + // Good file - rotate it into the backup slot. + fs.copyFileSync(DB_FILE, DB_BACKUP); + console.log('[DB] Backed up database to', DB_BACKUP); + + return data; + } + + // The main file is corrupt or empty. + console.warn(`[DB] ${DB_FILE} appears corrupt (${data.length} bytes) - checking backup`); + + if (fs.existsSync(DB_BACKUP)) { + const backup = new Uint8Array(fs.readFileSync(DB_BACKUP)); + + if (isValidSqlite(backup)) { + fs.copyFileSync(DB_BACKUP, DB_FILE); + console.warn('[DB] Restored database from backup', DB_BACKUP); + + return backup; + } + + console.error('[DB] Backup is also invalid - starting with a fresh database'); + } else { + console.error('[DB] No backup available - starting with a fresh database'); + } + + return undefined; +} + function resolveSqlJsConfig(): { locateFile: (file: string) => string } { return { locateFile: (file) => { @@ -47,10 +113,7 @@ export async function initDatabase(): Promise { if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); - let database: Uint8Array | undefined; - - if (fs.existsSync(DB_FILE)) - database = fs.readFileSync(DB_FILE); + const database = safeguardDbFile(); try { applicationDataSource = new DataSource({ @@ -94,7 +157,7 @@ export async function initDatabase(): Promise { await applicationDataSource.runMigrations(); console.log('[DB] Migrations executed'); } else { - console.log('[DB] Synchronize mode — migrations skipped'); + console.log('[DB] Synchronize mode - migrations skipped'); } } diff --git a/server/src/index.ts b/server/src/index.ts index 71dc2a2..1bc133a 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -9,7 +9,7 @@ import { resolveCertificateDirectory, resolveEnvFilePath } from './runtime-paths // Load .env from project root (one level up from server/) dotenv.config({ path: resolveEnvFilePath() }); -import { initDatabase } from './db/database'; +import { initDatabase, destroyDatabase } from './db/database'; import { deleteStaleJoinRequests } from './cqrs'; import { createApp } from './app'; import { @@ -119,6 +119,26 @@ async function bootstrap(): Promise { } } +let shuttingDown = false; + +async function gracefulShutdown(signal: string): Promise { + if (shuttingDown) return; + shuttingDown = true; + + console.log(`\n[Shutdown] ${signal} received — closing database…`); + + try { + await destroyDatabase(); + } catch (err) { + console.error('[Shutdown] Error closing database:', err); + } + + process.exit(0); +} + +process.on('SIGINT', () => gracefulShutdown('SIGINT')); +process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); + bootstrap().catch((err) => { console.error('Failed to start server:', err); process.exit(1);