import Database from 'better-sqlite3' import { mkdirSync, readdirSync, unlinkSync } from 'fs' import path from 'path' import { fileURLToPath } from 'url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const dataDir = path.join(__dirname, 'data') mkdirSync(dataDir, { recursive: true }) const db = new Database(path.join(dataDir, 'stories.db')) db.pragma('journal_mode = WAL') db.pragma('foreign_keys = ON') db.pragma('busy_timeout = 5000') // wait up to 5 s instead of failing immediately // Merge the WAL back into the main DB file once an hour. // Without this, all writes live in the WAL indefinitely, and losing // that file means losing all data written since the last checkpoint. setInterval(() => { try { db.pragma('wal_checkpoint(PASSIVE)') } catch (err) { console.warn('[db] WAL checkpoint failed:', err.message) } }, 60 * 60 * 1000) // every hour // Daily backup — keeps the last 7 daily snapshots in data/backups/ const backupDir = path.join(dataDir, 'backups') mkdirSync(backupDir, { recursive: true }) async function runBackup() { try { const stamp = new Date().toISOString().slice(0, 10) // YYYY-MM-DD const dest = path.join(backupDir, `stories-${stamp}.db`) await db.backup(dest) console.log(`[db] Backup written → ${dest}`) // Prune backups older than 7 days const files = readdirSync(backupDir) .filter(f => f.startsWith('stories-') && f.endsWith('.db')) .sort() for (const old of files.slice(0, -7)) { try { unlinkSync(path.join(backupDir, old)) } catch {} } } catch (err) { console.warn('[db] Backup failed:', err.message) } } // Run once on startup, then every 24 h runBackup() setInterval(runBackup, 24 * 60 * 60 * 1000) db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, username TEXT UNIQUE NOT NULL COLLATE NOCASE, password TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS stories ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, title TEXT NOT NULL DEFAULT 'Untitled Story', content TEXT DEFAULT '{}', cover_image TEXT, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS images ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, filename TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, story_id INTEGER NOT NULL REFERENCES stories(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, title TEXT NOT NULL DEFAULT 'Untitled Note', content TEXT DEFAULT '{}', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `) // Migrate: rename email -> username for existing databases const userCols = db.pragma('table_info(users)').map(c => c.name) if (userCols.includes('email') && !userCols.includes('username')) { db.exec('ALTER TABLE users RENAME COLUMN email TO username') } export default db