114 lines
4.1 KiB
JavaScript
114 lines
4.1 KiB
JavaScript
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
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS characters (
|
|
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,
|
|
name TEXT NOT NULL DEFAULT 'New Character',
|
|
role TEXT NOT NULL DEFAULT 'supporting',
|
|
age TEXT DEFAULT '',
|
|
description TEXT DEFAULT '',
|
|
notes TEXT DEFAULT '',
|
|
photo TEXT DEFAULT NULL,
|
|
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')
|
|
}
|
|
|
|
// Migrate: add photo column to characters if missing (pre-existing table)
|
|
const charCols = db.pragma('table_info(characters)').map(c => c.name)
|
|
if (charCols.length > 0 && !charCols.includes('photo')) {
|
|
db.exec('ALTER TABLE characters ADD COLUMN photo TEXT DEFAULT NULL')
|
|
}
|
|
|
|
export default db
|