Files
backerup-website/frontend/lib/db.ts
T
Anders Böttcher 984b282a95 feat: add admin links management and updaterup page
- Implemented AdminLinksPage for managing link entries with admin role check.
- Created API routes for fetching, updating, and deleting link entries.
- Added ProductTabs component for navigation between Backerup and Updaterup.
- Developed UpdaterupPage with detailed features, setup instructions, and statistics.
- Introduced links database functionality for runtime-editable link overrides.
2026-05-13 10:53:21 +02:00

134 lines
4.8 KiB
TypeScript

/**
* lib/db.ts
* Singleton libSQL (SQLite) client. Call getDb() anywhere on the server.
*
* WAL journal mode is set for better concurrency.
* On first boot, existing meta.json / users.json are imported and renamed
* to *.migrated so the one-time import never runs twice.
*/
import { createClient, type Client } from '@libsql/client'
import path from 'path'
import fs from 'fs'
const DATA_DIR = path.join(process.cwd(), 'data')
const DB_URL = `file:${path.join(DATA_DIR, 'backerup.db')}`
let _client: Client | null = null
export function getDb(): Client {
if (_client) return _client
fs.mkdirSync(DATA_DIR, { recursive: true })
_client = createClient({ url: DB_URL })
// Fire-and-forget: migrations run before first real query via ensureReady()
return _client
}
/** Call once at startup (or await before first query if needed). */
let _ready: Promise<void> | null = null
export function ensureReady(): Promise<void> {
if (_ready) return _ready
_ready = (async () => {
const db = getDb()
await applyMigrations(db)
await importLegacyJson(db)
})()
return _ready
}
// ── Schema ────────────────────────────────────────────────────────────────────
async function applyMigrations(db: Client): Promise<void> {
await db.executeMultiple(`
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
PRAGMA synchronous = NORMAL;
CREATE TABLE IF NOT EXISTS files (
token TEXT PRIMARY KEY,
original_name TEXT NOT NULL,
display_name TEXT,
size INTEGER NOT NULL,
type TEXT NOT NULL,
uploaded_at TEXT NOT NULL,
description TEXT,
group_name TEXT,
container_name TEXT,
image TEXT
);
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
password_hash TEXT NOT NULL,
salt TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS links (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`)
}
// ── One-time JSON → SQLite import ─────────────────────────────────────────────
async function importLegacyJson(db: Client): Promise<void> {
const metaPath = path.join(DATA_DIR, 'meta.json')
if (fs.existsSync(metaPath)) {
try {
const { rows } = await db.execute('SELECT COUNT(*) as cnt FROM files')
if ((rows[0].cnt as number) === 0) {
type OldRecord = {
token: string; originalName: string; displayName?: string; size: number
type: string; uploadedAt: string; description?: string; group?: string
containerName?: string; image?: string
}
const records = JSON.parse(fs.readFileSync(metaPath, 'utf-8')) as OldRecord[]
for (const r of records) {
await db.execute({
sql: `INSERT OR IGNORE INTO files
(token,original_name,display_name,size,type,uploaded_at,description,group_name,container_name,image)
VALUES (?,?,?,?,?,?,?,?,?,?)`,
args: [r.token, r.originalName, r.displayName ?? null, r.size, r.type,
r.uploadedAt, r.description ?? null, r.group ?? null,
r.containerName ?? null, r.image ?? null],
})
}
fs.renameSync(metaPath, metaPath + '.migrated')
console.log(`[db] Imported ${records.length} file records from meta.json`)
}
} catch (e) {
console.error('[db] Failed to import meta.json:', e)
}
}
const usersPath = path.join(DATA_DIR, 'users.json')
if (fs.existsSync(usersPath)) {
try {
const { rows } = await db.execute('SELECT COUNT(*) as cnt FROM users')
if ((rows[0].cnt as number) === 0) {
type OldUser = {
id: string; username: string; passwordHash: string; salt: string
role: string; createdAt: string
}
const users = JSON.parse(fs.readFileSync(usersPath, 'utf-8')) as OldUser[]
for (const u of users) {
await db.execute({
sql: `INSERT OR IGNORE INTO users (id,username,password_hash,salt,role,created_at)
VALUES (?,?,?,?,?,?)`,
args: [u.id, u.username, u.passwordHash, u.salt, u.role, u.createdAt],
})
}
fs.renameSync(usersPath, usersPath + '.migrated')
console.log(`[db] Imported ${users.length} users from users.json`)
}
} catch (e) {
console.error('[db] Failed to import users.json:', e)
}
}
}