import fs from 'fs'; import path from 'path'; import { DataSource } from 'typeorm'; import { AuthUserEntity, ServerEntity, ServerTagEntity, ServerChannelEntity, ServerRoleEntity, ServerUserRoleEntity, ServerChannelPermissionEntity, JoinRequestEntity, ServerMembershipEntity, ServerInviteEntity, ServerBanEntity } from '../entities'; import { serverMigrations } from '../migrations'; import { findExistingPath, resolveRuntimePath } from '../runtime-paths'; 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) => { const bundledBinaryPath = path.join(__dirname, '..', '..', 'node_modules', 'sql.js', 'dist', file); return findExistingPath( resolveRuntimePath(file), bundledBinaryPath ) ?? bundledBinaryPath; } }; } export function getDataSource(): DataSource { if (!applicationDataSource?.isInitialized) { throw new Error('DataSource not initialised'); } return applicationDataSource; } export async function initDatabase(): Promise { if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); const database = safeguardDbFile(); try { applicationDataSource = new DataSource({ type: 'sqljs', database, entities: [ AuthUserEntity, ServerEntity, ServerTagEntity, ServerChannelEntity, ServerRoleEntity, ServerUserRoleEntity, ServerChannelPermissionEntity, JoinRequestEntity, ServerMembershipEntity, ServerInviteEntity, ServerBanEntity ], migrations: serverMigrations, synchronize: process.env.DB_SYNCHRONIZE === 'true', logging: false, autoSave: true, location: DB_FILE, sqlJsConfig: resolveSqlJsConfig() }); } catch (error) { console.error('[DB] Failed to configure the sql.js data source', error); throw error; } try { await applicationDataSource.initialize(); } catch (error) { console.error('[DB] Failed to initialise the sql.js data source', error); throw error; } console.log('[DB] Connection initialised at:', DB_FILE); if (process.env.DB_SYNCHRONIZE !== 'true') { await applicationDataSource.runMigrations(); console.log('[DB] Migrations executed'); } else { console.log('[DB] Synchronize mode - migrations skipped'); } } export async function destroyDatabase(): Promise { if (applicationDataSource?.isInitialized) { await applicationDataSource.destroy(); applicationDataSource = undefined; console.log('[DB] Connection closed'); } }