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/server.mjs8 * Purpose : Fastify bootstrap — plugins, auth hook, static/vendor assets,9 * healthz, daily maintenance, graceful shutdown10 * License : MIT © Simon-Pierre Boucher11 * ─────────────────────────────────────────────12 */1314import path from 'node:path';15import { fileURLToPath } from 'node:url';16import Fastify from 'fastify';17import fastifyCookie from '@fastify/cookie';18import fastifyFormbody from '@fastify/formbody';19import fastifyStatic from '@fastify/static';20import { config, ensureDataDirs } from './config.mjs';21import { ensureAuthBootstrap } from './auth/password.mjs';22import { SESSION_COOKIE, getApiToken, getSession, pruneSessions } from './auth/session.mjs';23import { getDb } from './db/db.mjs';24import { gcBlobs, storageStats } from './storage/blobs.mjs';25import { purgeTrash } from './storage/nodes.mjs';26import { pruneUploads } from './storage/upload.mjs';27import { pruneShares } from './shares/shares.mjs';28import { pruneRequests } from './shares/requests.mjs';29import { vacuumFts } from './search/search.mjs';30import { scheduleMissingExtractions } from './search/extract.mjs';31import { queueDepth } from './preview/queue.mjs';32import { registerApiV1 } from './api/v1.mjs';33import { registerWebRoutes } from './web/routes.mjs';3435const HERE = path.dirname(fileURLToPath(import.meta.url));36const startedAt = Date.now();3738async function main() {39 ensureDataDirs();40 getDb();41 await ensureAuthBootstrap();4243 const app = Fastify({44 logger: {45 level: config.logLevel,46 redact: {47 paths: [48 'req.headers.authorization', 'req.headers.cookie',49 'res.headers["set-cookie"]',50 ],51 censor: '[redacted]',52 },53 },54 trustProxy: config.trustProxy,55 bodyLimit: 32 * 1024 * 1024,56 });5758 await app.register(fastifyCookie);59 await app.register(fastifyFormbody);6061 // Upload chunks arrive as raw octet streams; handlers read req.raw directly.62 app.addContentTypeParser('application/octet-stream', (req, payload, done) => done(null, null));63 // The in-browser editor saves file content as text/plain.64 app.addContentTypeParser('text/plain', { parseAs: 'buffer' }, (req, body, done) => done(null, body));6566 // ── Authentication decorator (session cookie or Bearer API token) ────67 app.decorateRequest('authed', false);68 app.decorateRequest('authKind', '');69 app.addHook('onRequest', async (req) => {70 const bearer = req.headers.authorization?.match(/^Bearer\s+(.+)$/i)?.[1];71 if (bearer && getApiToken(bearer)) {72 req.authed = true;73 req.authKind = 'token';74 return;75 }76 const session = getSession(req.cookies?.[SESSION_COOKIE]);77 if (session) {78 req.authed = true;79 req.authKind = 'session';80 }81 });8283 // ── Static assets (immutable app code + self-hosted vendor bundles) ──84 await app.register(fastifyStatic, {85 root: path.join(HERE, 'web', 'assets'),86 prefix: '/assets/',87 maxAge: '1h',88 cacheControl: true,89 });90 const vendor = (prefix, ...segments) =>91 app.register(fastifyStatic, {92 root: path.resolve(HERE, '..', 'node_modules', ...segments),93 prefix, decorateReply: false, maxAge: '7d',94 });95 await vendor('/vendor/pdfjs/', 'pdfjs-dist', 'build');96 await vendor('/vendor/wavesurfer/', 'wavesurfer.js', 'dist');97 await vendor('/vendor/xlsx/', 'xlsx', 'dist');98 await vendor('/vendor/fonts/inter/', '@fontsource', 'inter', 'files');99 await vendor('/vendor/fonts/jetbrains-mono/', '@fontsource', 'jetbrains-mono', 'files');100101 // ── Health ────────────────────────────────────────────────────────────102 app.get('/healthz', async () => {103 const stats = storageStats();104 return {105 ok: true,106 uptimeSec: Math.floor((Date.now() - startedAt) / 1000),107 nodes: stats.nodeCount,108 usedBytes: stats.usedBytes,109 queueDepth: queueDepth(),110 };111 });112113 registerApiV1(app);114 registerWebRoutes(app);115116 // ── Daily maintenance ────────────────────────────────────────────────117 const maintenance = async () => {118 try {119 const purged = purgeTrash(config.trashRetentionDays);120 const gcd = await gcBlobs();121 const shares = pruneShares();122 const requests = pruneRequests();123 const sessions = pruneSessions();124 const uploads = await pruneUploads();125 vacuumFts();126 const queued = scheduleMissingExtractions();127 app.log.info({ purged, gcd, shares, requests, sessions, uploads, queued }, 'maintenance done');128 } catch (err) {129 app.log.error({ err }, 'maintenance failed');130 }131 };132 setTimeout(maintenance, 30_000).unref();133 setInterval(maintenance, 24 * 3_600_000).unref();134135 await app.listen({ port: config.port, host: config.host });136 app.log.info(`SPB Drive listening on http://${config.host}:${config.port} → ${config.publicUrl}`);137138 const shutdown = async (signal) => {139 app.log.info({ signal }, 'shutting down');140 await app.close();141 process.exit(0);142 };143 process.on('SIGINT', () => shutdown('SIGINT'));144 process.on('SIGTERM', () => shutdown('SIGTERM'));145}146147main().catch((err) => {148 console.error('Fatal boot error:', err);149 process.exit(1);150});151