SPB Git

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%
ZIP
NameLast commitUpdated
cli feat: tests (unit+e2e), eslint, deploy artifacts (setup-m3u96b, pm2,... yesterday
deploy feat: production deploy to m3u96b — ~/srv/drive layout, ngrok config... yesterday
scripts feat: tests (unit+e2e), eslint, deploy artifacts (setup-m3u96b, pm2,... yesterday
src feat: v2.1 design overhaul — light-first 'Ivoire & Indigo' identity +... 4 h ago
test feat: v2.0 — file versioning, in-browser editor, file requests,... 5 h ago
.gitignore feat: skeleton, auth, storage core — config, sqlite schema, blob CAS,... yesterday
CLAUDE.md chore: redact bootstrap password literal from tracked spec per §0.2 yesterday
eslint.config.mjs feat: tests (unit+e2e), eslint, deploy artifacts (setup-m3u96b, pm2,... yesterday
package-lock.json feat: tests (unit+e2e), eslint, deploy artifacts (setup-m3u96b, pm2,... yesterday
package.json chore: bump to 2.1.0 4 h ago
README.md feat: v2.0 — file versioning, in-browser editor, file requests,... 5 h ago
README.md

# SPB Drive

Node Fastify SQLite Version Tests License

Personal cloud drive of Simon-Pierre Boucher — a self-hosted Google Drive / Dropbox replacement for one person. Private by default behind a password; the only public surfaces are explicitly created share links.

Live: https://drive.spboucher.ai (node m3u96b · ngrok custom domain)

# Highlights

  • Vault — content-addressed blob store (sha256 CAS → free dedup), unlimited nested folders, chunked + resumable uploads (8 MB chunks), drag-drop of entire folder trees, paste-to-upload, trash with 30-day retention, tags, stars, folder colors/emoji.
  • Universal previewer — images (zoom/pan/EXIF/GPS), video (Range streaming with instant seek; mkv/avi/hevc transcoded to H.264 in the background), audio (waveform + ID3 cover art), PDF (pdf.js with page rail, text selection + in-doc search), Office → PDF via LibreOffice headless, xlsx native table fast path, code (Shiki, 100+ languages), Markdown GFM, CSV (100k rows, sortable), JSON tree, ipynb, zip/tar browsing with inner-file preview, fonts, eml — and an elegant fallback card for everything else.
  • Sharing machinehttps://drive.spboucher.ai/s/<token> (10-char base58, ≥58 bits), optional expiry / argon2 password / max downloads / preview-only, folder shares with ZIP-all streaming, Open Graph unfurls, server-side SVG QR codes, per-share visit/download log, instant revocation.
  • Search — SQLite FTS5 over names, tags and file contents (pdftotext, office→text, code).
  • CLIspbdrive init/ls/up/down/mkdir/mv/rm/restore/share/shares/revoke/search/push/doctor.

# New in v2.0

  • File versioning — replacing or editing a file archives the previous content (up to 20 versions per file, CAS-backed so identical bytes cost nothing). Browse, download, restore or delete versions from the Info panel / right-click → Version history. Upload conflicts now offer Keep both / Replace (new version) / Skip, batch-applicable.
  • In-browser text editor — edit code / Markdown / JSON / CSV directly (⌘S saves as a new version), plus New → Text file. Keyboard shortcut E.
  • File requestshttps://drive.spboucher.ai/r/<token> (12-char base58, ~70 bits) lets anyone send files straight into a folder of your choice without seeing its contents: optional expiry + max-file quota, QR code, instant close, everything logged in Activity.
  • Storage insights — dedup savings, version-history footprint, trash weight, top-20 largest files and duplicate-file groups with one-click cleanup (sidebar → Storage).

# Layout

text
src/         Fastify server (auth, storage, preview, shares, search, api, web)
cli/         spbdrive CLI (API-token auth, chunked uploads, push sync)
deploy/      setup-m3u96b.sh · ecosystem.config.cjs (pm2) · ngrok.yml · backup.sh · systemd unit
scripts/     check-headers · inject-headers · check-secrets · reset-password
test/        vitest unit + e2e (upload → thumb → protected share → download)

Runtime data lives outside the repo at ~/srv/drive on m3u96b (macOS seals /srv; on Linux use /srv/drive — the systemd unit keeps that path) (files/ CAS blobs · db/drive.sqlite · cache/ thumbs+transcodes · backups/ · logs/).

# Development

bash
npm install
SPBDRIVE_DATA_DIR=./data SPBDRIVE_BOOTSTRAP_PASSWORD=devpass npm run dev
# → http://127.0.0.1:7430
npm run check      # lint + headers + secrets + tests

The bootstrap password is consumed once on first boot (argon2id → data/auth.json) and the env var is ignored forever after. Break-glass reset: node scripts/reset-password.mjs (local TTY).

# Deployment (m3u96b)

bash
rsync -a --exclude node_modules --exclude .git . m3u96b:~/spbdrive-src/
ssh m3u96b 'SPBDRIVE_BOOTSTRAP_PASSWORD=… bash ~/spbdrive-src/deploy/setup-m3u96b.sh ~/spbdrive-src'

The script is idempotent: installs ffmpeg/LibreOffice/poppler/7z/ngrok/pm2, creates ~/srv/drive (700), syncs the app to ~/srv/drive/app, npm ci --omit=dev, seeds auth on first boot, starts spbdrive-server + spbdrive-tunnel under pm2 (pm2 save + pm2 startup for reboot survival), health-checks :7430/healthz, and installs the nightly backup cron (02:30).

DNS/ngrok (one-time): reserve drive.spboucher.ai in the ngrok dashboard and add the CNAME it gives you at the spboucher.ai DNS provider. TLS terminates at ngrok; the app trusts x-forwarded-for for lockout + share logs.

# Backups & restore

deploy/backup.sh (cron, 02:30) writes ~/srv/drive/backups/YYYY-MM-DD/: a SQLite .backup snapshot plus an rsync --link-dest hardlink copy of the blob store (unchanged blobs cost zero bytes). Retention: 14 dailies + 8 Sunday weeklies. Each run verifies PRAGMA integrity_check and blob counts.

Restore procedure (tested):

bash
pm2 stop spbdrive-server
DRIVE=~/srv/drive
SNAP=$DRIVE/backups/<date>
cp "$SNAP/drive.sqlite" $DRIVE/db/drive.sqlite
rm -f $DRIVE/db/drive.sqlite-wal $DRIVE/db/drive.sqlite-shm
rsync -a --delete "$SNAP/files/" $DRIVE/files/
cp -p "$SNAP/auth.json" "$SNAP/keys.json" $DRIVE/
pm2 start spbdrive-server && curl -fsS http://127.0.0.1:7430/healthz

# Security model

  • Single argon2id-hashed password; 5 failures → 15-min per-IP lockout + global backoff.
  • Sessions: 256-bit ids stored hashed; cookies httpOnly · Secure · SameSite=Lax; sliding TTL.
  • CSRF: double-submit token on form posts, custom header on JSON API writes.
  • Share links: unguessable base58 tokens, constant-time compares, argon2 share passwords, signed gate cookies; expired/revoked pages leak nothing.
  • Nodes are DB ids and blobs are hashes — client paths are never trusted; names validated against ^[^/\\\0]{1,255}$.
  • Uploaded HTML downloads as attachment; SVG previews render under CSP: sandbox; strict default-src 'self' CSP app-wide; no third-party requests anywhere (fonts/pdf.js/wavesurfer self-hosted).
  • ffmpeg/LibreOffice run via execFile (no shell) with timeouts; pino redacts auth headers.

# License

MIT © Simon-Pierre Boucher — contact@spboucher.ai