mirror of
https://gitlab.w-hs.de/an14051/backerup-website.git
synced 2026-07-27 17:35:29 +00:00
33e339bde6
- Removed community sharing and self-hosted features from the features list in page.tsx. - Updated footer text in layout.tsx to remove the MIT License mention. - Added @libsql/client dependency to package.json and bun.lock for database management. - Implemented a new db.ts file to handle SQLite database connections and migrations. - Refactored user and file management functions in users.ts and store.ts to utilize the new database client. - Ensured legacy JSON data is imported into the new SQLite database structure on first run.
128 lines
4.6 KiB
TypeScript
128 lines
4.6 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
|
|
);
|
|
`)
|
|
}
|
|
|
|
// ── 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)
|
|
}
|
|
}
|
|
}
|