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 : README.md8 Purpose : Project overview, ops runbook, restore procedure9 License : MIT © Simon-Pierre Boucher10 ─────────────────────────────────────────────11-->1213# SPB Drive141516171819202122**Personal cloud drive of [Simon-Pierre Boucher](mailto:contact@spboucher.ai)** — a self-hosted23Google Drive / Dropbox replacement for one person. Private by default behind a password;24the only public surfaces are explicitly created share links.2526**Live:** https://drive.spboucher.ai (node m3u96b · ngrok custom domain)2728## Highlights2930- **Vault** — content-addressed blob store (sha256 CAS → free dedup), unlimited nested folders,31 chunked + resumable uploads (8 MB chunks), drag-drop of entire folder trees, paste-to-upload,32 trash with 30-day retention, tags, stars, folder colors/emoji.33- **Universal previewer** — images (zoom/pan/EXIF/GPS), video (Range streaming with instant seek;34 mkv/avi/hevc transcoded to H.264 in the background), audio (waveform + ID3 cover art), PDF35 (pdf.js with page rail, text selection + in-doc search), Office → PDF via LibreOffice headless,36 xlsx native table fast path, code (Shiki, 100+ languages), Markdown GFM, CSV (100k rows,37 sortable), JSON tree, ipynb, zip/tar browsing with inner-file preview, fonts, eml — and an38 elegant fallback card for everything else.39- **Sharing machine** — `https://drive.spboucher.ai/s/<token>` (10-char base58, ≥58 bits),40 optional expiry / argon2 password / max downloads / preview-only, folder shares with41 ZIP-all streaming, Open Graph unfurls, server-side SVG QR codes, per-share visit/download log,42 instant revocation.43- **Search** — SQLite FTS5 over names, tags **and file contents** (pdftotext, office→text, code).44- **CLI** — `spbdrive init/ls/up/down/mkdir/mv/rm/restore/share/shares/revoke/search/push/doctor`.4546### New in v2.04748- **File versioning** — replacing or editing a file archives the previous content49 (up to 20 versions per file, CAS-backed so identical bytes cost nothing). Browse,50 download, restore or delete versions from the Info panel / right-click → *Version history*.51 Upload conflicts now offer **Keep both / Replace (new version) / Skip**, batch-applicable.52- **In-browser text editor** — edit code / Markdown / JSON / CSV directly (⌘S saves as a new53 version), plus *New → Text file*. Keyboard shortcut `E`.54- **File requests** — `https://drive.spboucher.ai/r/<token>` (12-char base58, ~70 bits) lets55 anyone **send** files straight into a folder of your choice without seeing its contents:56 optional expiry + max-file quota, QR code, instant close, everything logged in Activity.57- **Storage insights** — dedup savings, version-history footprint, trash weight, top-2058 largest files and duplicate-file groups with one-click cleanup (sidebar → *Storage*).5960## Layout6162```63src/ Fastify server (auth, storage, preview, shares, search, api, web)64cli/ spbdrive CLI (API-token auth, chunked uploads, push sync)65deploy/ setup-m3u96b.sh · ecosystem.config.cjs (pm2) · ngrok.yml · backup.sh · systemd unit66scripts/ check-headers · inject-headers · check-secrets · reset-password67test/ vitest unit + e2e (upload → thumb → protected share → download)68```6970Runtime data lives outside the repo at `~/srv/drive` on m3u96b (macOS seals `/srv`; on Linux use `/srv/drive` — the systemd unit keeps that path)71(`files/` CAS blobs · `db/drive.sqlite` · `cache/` thumbs+transcodes · `backups/` · `logs/`).7273## Development7475```bash76npm install77SPBDRIVE_DATA_DIR=./data SPBDRIVE_BOOTSTRAP_PASSWORD=devpass npm run dev78# → http://127.0.0.1:743079npm run check # lint + headers + secrets + tests80```8182The bootstrap password is consumed once on first boot (argon2id → `data/auth.json`) and the83env var is ignored forever after. Break-glass reset: `node scripts/reset-password.mjs` (local TTY).8485## Deployment (m3u96b)8687```bash88rsync -a --exclude node_modules --exclude .git . m3u96b:~/spbdrive-src/89ssh m3u96b 'SPBDRIVE_BOOTSTRAP_PASSWORD=… bash ~/spbdrive-src/deploy/setup-m3u96b.sh ~/spbdrive-src'90```9192The script is idempotent: installs ffmpeg/LibreOffice/poppler/7z/ngrok/pm2, creates `~/srv/drive`93(700), syncs the app to `~/srv/drive/app`, `npm ci --omit=dev`, seeds auth on first boot, starts94`spbdrive-server` + `spbdrive-tunnel` under pm2 (`pm2 save` + `pm2 startup` for reboot survival),95health-checks `:7430/healthz`, and installs the nightly backup cron (02:30).9697DNS/ngrok (one-time): reserve `drive.spboucher.ai` in the ngrok dashboard and add the CNAME it98gives you at the spboucher.ai DNS provider. TLS terminates at ngrok; the app trusts99`x-forwarded-for` for lockout + share logs.100101## Backups & restore102103`deploy/backup.sh` (cron, 02:30) writes `~/srv/drive/backups/YYYY-MM-DD/`:104a SQLite `.backup` snapshot plus an rsync `--link-dest` hardlink copy of the blob store105(unchanged blobs cost zero bytes). Retention: 14 dailies + 8 Sunday weeklies. Each run verifies106`PRAGMA integrity_check` and blob counts.107108**Restore procedure** (tested):109110```bash111pm2 stop spbdrive-server112DRIVE=~/srv/drive113SNAP=$DRIVE/backups/<date>114cp "$SNAP/drive.sqlite" $DRIVE/db/drive.sqlite115rm -f $DRIVE/db/drive.sqlite-wal $DRIVE/db/drive.sqlite-shm116rsync -a --delete "$SNAP/files/" $DRIVE/files/117cp -p "$SNAP/auth.json" "$SNAP/keys.json" $DRIVE/118pm2 start spbdrive-server && curl -fsS http://127.0.0.1:7430/healthz119```120121## Security model122123- Single argon2id-hashed password; 5 failures → 15-min per-IP lockout + global backoff.124- Sessions: 256-bit ids stored **hashed**; cookies `httpOnly · Secure · SameSite=Lax`; sliding TTL.125- CSRF: double-submit token on form posts, custom header on JSON API writes.126- Share links: unguessable base58 tokens, constant-time compares, argon2 share passwords,127 signed gate cookies; expired/revoked pages leak nothing.128- Nodes are DB ids and blobs are hashes — client paths are never trusted; names validated129 against `^[^/\\\0]{1,255}$`.130- Uploaded HTML downloads as attachment; SVG previews render under `CSP: sandbox`; strict131 `default-src 'self'` CSP app-wide; no third-party requests anywhere (fonts/pdf.js/wavesurfer132 self-hosted).133- ffmpeg/LibreOffice run via `execFile` (no shell) with timeouts; pino redacts auth headers.134135## License136137MIT © Simon-Pierre Boucher — [contact@spboucher.ai](mailto:contact@spboucher.ai)138