SPB Drive
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 machine —
https://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).
- CLI —
spbdrive 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 requests —
https://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
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
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 + testsThe 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)
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):
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/healthzSecurity 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; strictdefault-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