spb/drive Public
SPB Drive — self-hosted personal cloud drive (files, previews, sharing) on the MacLustr cluster.
JavaScript 82.7%
CSS 10.6%
Nunjucks 3.6%
Shell 1.8%
SQL 1.3%
1/**2 * ─────────────────────────────────────────────3 * SPB Drive — Personal Cloud Drive4 * ─────────────────────────────────────────────5 * Author : Simon-Pierre Boucher6 * Contact : contact@spboucher.ai7 * File : src/db/db.mjs8 * Purpose : SQLite bootstrap (better-sqlite3, WAL), migrations, root node seed9 * License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { readFileSync } from 'node:fs';14import path from 'node:path';15import { fileURLToPath } from 'node:url';16import Database from 'better-sqlite3';17import { config } from '../config.mjs';1819const HERE = path.dirname(fileURLToPath(import.meta.url));2021/** @type {import('better-sqlite3').Database | null} */22let db = null;2324/**25 * Open (or return the already-open) database, applying schema migrations26 * and seeding the root folder node on first run.27 * @param {string} [file] Override DB path (tests).28 * @returns {import('better-sqlite3').Database}29 */30export function getDb(file) {31 if (db) return db;32 db = new Database(file ?? config.dbFile);33 db.pragma('journal_mode = WAL');34 db.pragma('foreign_keys = ON');35 db.pragma('synchronous = NORMAL');3637 db.exec(readFileSync(path.join(HERE, 'schema.sql'), 'utf8'));38 migrate(db);3940 const root = db.prepare('SELECT id FROM nodes WHERE id = 1').get();41 if (!root) {42 db.prepare(43 `INSERT INTO nodes (id, parent_id, name, type, created, modified)44 VALUES (1, NULL, '', 'folder', ?, ?)`,45 ).run(Date.now(), Date.now());46 }47 return db;48}4950/**51 * Column-level migrations for tables that predate v2 (CREATE IF NOT EXISTS52 * won't touch existing tables, so new columns are added here).53 */54function migrate(database) {55 const addColumn = (table, column, ddl) => {56 const present = database57 .prepare(`SELECT 1 FROM pragma_table_info(?) WHERE name = ?`)58 .get(table, column);59 if (!present) database.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddl}`);60 };61 // v2: public file-request uploads are tagged with their request id.62 addColumn('uploads', 'source', 'TEXT');63}6465/** Close the database (tests / graceful shutdown). */66export function closeDb() {67 if (db) {68 db.close();69 db = null;70 }71}7273/**74 * Record an activity-log entry.75 * @param {string} kind e.g. 'login', 'upload', 'rename', 'share.create'76 * @param {{nodeId?: number|null, detail?: string, ip?: string}} [extra]77 */78export function logActivity(kind, { nodeId = null, detail = '', ip = '' } = {}) {79 getDb()80 .prepare('INSERT INTO activity (ts, kind, node_id, detail, ip) VALUES (?, ?, ?, ?, ?)')81 .run(Date.now(), kind, nodeId, detail, ip);82}83