# SPB Drive ![Node](https://img.shields.io/badge/node-%E2%89%A520-339933?logo=node.js&logoColor=white) ![Fastify](https://img.shields.io/badge/fastify-5-000000?logo=fastify) ![SQLite](https://img.shields.io/badge/sqlite-FTS5%20%2B%20WAL-003B57?logo=sqlite) ![Version](https://img.shields.io/badge/version-2.0.0-b48cff) ![Tests](https://img.shields.io/badge/tests-27%20passing-22d3aa) ![License](https://img.shields.io/badge/license-MIT-4f8cff) **Personal cloud drive of [Simon-Pierre Boucher](mailto:contact@spboucher.ai)** — 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/` (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/` (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 ```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/ 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](mailto:contact@spboucher.ai)