1 Commits

Author SHA1 Message Date
Myx
f3b56fb1cc fix: Db corruption fix
All checks were successful
Queue Release Build / prepare (push) Successful in 17s
Deploy Web Apps / deploy (push) Successful in 10m0s
Queue Release Build / build-linux (push) Successful in 25m59s
Queue Release Build / build-windows (push) Successful in 21m44s
Queue Release Build / finalize (push) Successful in 18s
2026-04-13 02:23:09 +02:00
2 changed files with 91 additions and 8 deletions

View File

@@ -17,11 +17,77 @@ import {
import { serverMigrations } from '../migrations'; import { serverMigrations } from '../migrations';
import { findExistingPath, resolveRuntimePath } from '../runtime-paths'; import { findExistingPath, resolveRuntimePath } from '../runtime-paths';
const DATA_DIR = resolveRuntimePath('data'); function resolveDbFile(): string {
const DB_FILE = path.join(DATA_DIR, 'metoyou.sqlite'); 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; 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 } { function resolveSqlJsConfig(): { locateFile: (file: string) => string } {
return { return {
locateFile: (file) => { locateFile: (file) => {
@@ -47,10 +113,7 @@ export async function initDatabase(): Promise<void> {
if (!fs.existsSync(DATA_DIR)) if (!fs.existsSync(DATA_DIR))
fs.mkdirSync(DATA_DIR, { recursive: true }); fs.mkdirSync(DATA_DIR, { recursive: true });
let database: Uint8Array | undefined; const database = safeguardDbFile();
if (fs.existsSync(DB_FILE))
database = fs.readFileSync(DB_FILE);
try { try {
applicationDataSource = new DataSource({ applicationDataSource = new DataSource({
@@ -94,7 +157,7 @@ export async function initDatabase(): Promise<void> {
await applicationDataSource.runMigrations(); await applicationDataSource.runMigrations();
console.log('[DB] Migrations executed'); console.log('[DB] Migrations executed');
} else { } else {
console.log('[DB] Synchronize mode migrations skipped'); console.log('[DB] Synchronize mode - migrations skipped');
} }
} }

View File

@@ -9,7 +9,7 @@ import { resolveCertificateDirectory, resolveEnvFilePath } from './runtime-paths
// Load .env from project root (one level up from server/) // Load .env from project root (one level up from server/)
dotenv.config({ path: resolveEnvFilePath() }); dotenv.config({ path: resolveEnvFilePath() });
import { initDatabase } from './db/database'; import { initDatabase, destroyDatabase } from './db/database';
import { deleteStaleJoinRequests } from './cqrs'; import { deleteStaleJoinRequests } from './cqrs';
import { createApp } from './app'; import { createApp } from './app';
import { import {
@@ -119,6 +119,26 @@ async function bootstrap(): Promise<void> {
} }
} }
let shuttingDown = false;
async function gracefulShutdown(signal: string): Promise<void> {
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) => { bootstrap().catch((err) => {
console.error('Failed to start server:', err); console.error('Failed to start server:', err);
process.exit(1); process.exit(1);