# CLAUDE.md — **SPB Drive** · Personal Cloud Drive for Simon-Pierre Boucher > **Read this entire document before writing a single line of code.** > This file is the single source of truth. Every architectural decision, naming convention, UI detail, and non-negotiable rule lives here. When in doubt, re-read this file. When this file conflicts with your instinct, this file wins. --- ## 0. Identity & Non-Negotiables | Key | Value | |---|---| | **Product name** | SPB Drive | | **Owner / sole user** | Simon-Pierre Boucher | | **Contact** | [contact@spboucher.ai](mailto:contact@spboucher.ai) | | **Public domain** | `https://drive.spboucher.ai` (ngrok custom domain) | | **Deployment host** | Node **m3u96b** | | **Runtime** | Node.js ≥ 20, ESM only | | **Access model** | **Private by default.** The entire drive is behind a password login. The ONLY public surfaces are explicitly created **share links**. | | **Initial password** | *(redacted per §0.2 — supplied via `SPBDRIVE_BOOTSTRAP_PASSWORD` at first boot)* — seeded at first boot, **stored argon2-hashed, never in code, never in the repo, never logged**. Changeable from Settings. | ### 0.1 THE GOLDEN RULE — Mandatory Author Header **Every single file you generate — source, config, script, style, test, migration — MUST begin with an author header.** No exceptions. A file without this header is a bug. Canonical template (adapt comment syntax per language): ```js /** * ───────────────────────────────────────────── * SPB Drive — Personal Cloud Drive * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : * Purpose : * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ ``` | Language | Syntax | |---|---| | JS / TS / CSS / SCSS | `/** ... */` block | | Python / Bash / YAML / Dockerfile / TOML | `#` lines | | HTML / Nunjucks | `` / `{# ... #}` | | SQL | `-- ` lines | Provide `scripts/inject-headers.mjs` and `scripts/check-headers.mjs`; `npm run check:headers` must fail CI if any tracked file lacks the header. ### 0.2 Password / secret handling rules (absolute) - The literal bootstrap password string must appear **nowhere** in the codebase, config files, tests, fixtures, or logs. It is provided once via the `SPBDRIVE_BOOTSTRAP_PASSWORD` env var (or interactive prompt) on first boot, hashed with **argon2id**, stored in `data/auth.json`, and the env var is then ignored forever. - Password change flow in Settings (requires current password). Also a break-glass CLI on the server: `node scripts/reset-password.mjs` (interactive, local only). - Session secret, share-link signing key: generated randomly at first boot into `data/keys.json` (chmod 600). --- ## 1. Product Vision SPB Drive is a **self-hosted Google Drive / Dropbox replacement** for one person. Three purposes: 1. **Vault**: all of Simon-Pierre's files, organized in folders, uploadable from any browser, safe on m3u96b. 2. **Universal previewer**: click any file → beautiful in-browser preview. Images, video, audio, PDF, Office docs, code, Markdown, CSV, archives, fonts — *everything imaginable* previews without downloading. 3. **Sharing machine**: any file or folder → one click → clean public URL (`https://drive.spboucher.ai/s/`) with optional expiry, password, and download limits. Recipients need no account. Quality bar: *"If a stranger receives a share link, the preview page should look like a polished commercial product."* --- ## 2. High-Level Architecture ``` ┌──────────────────────────── node m3u96b ─────────────────────────────┐ │ │ │ ┌─────────────┐ ┌────────────────────────────────────────────────┐ │ │ │ ngrok │ │ SPB Drive Server (Node 20) │ │ │ │ tunnel │──▶│ Fastify app :7430 │ │ │ │ drive. │ │ ├─ /login, /app/* Web UI (SSR + JS) │ │ │ │ spboucher.ai │ │ ├─ /api/v1/* JSON API (session auth) │ │ │ └─────────────┘ │ ├─ /s/:token[/...] PUBLIC share pages │ │ │ │ ├─ /dl/*, /stream/* Auth'd download/stream │ │ │ │ └─ /thumb/* Thumbnails │ │ │ └────────────────┬───────────────────────────────┘ │ │ │ │ │ ┌──────────────────────────────┼──────────────────────────────┐ │ │ │ │ │ │ │ ┌────▼─────────────┐ ┌────────────▼───────────┐ ┌──────────────▼─┐ │ │ │ /srv/drive/files │ │ /srv/drive/db │ │ /srv/drive/ │ │ │ │ content store: │ │ drive.sqlite (better- │ │ cache/ │ │ │ │ blobs by sha256 │ │ sqlite3, WAL): │ │ thumbnails, │ │ │ │ /ab/cd/abcd... │ │ nodes, shares, tags, │ │ transcodes, │ │ │ │ (dedup natural) │ │ sessions, activity, FTS│ │ office→pdf │ │ │ └──────────────────┘ └────────────────────────┘ └────────────────┘ │ └────────────────────────────────────────────────────────────────────────┘ ``` **Principles:** - **Content-addressed blob store** (`/srv/drive/files///`): identical files stored once (free dedup); DB `nodes` table maps the virtual folder tree onto blobs. Deleting a node only deletes the blob when its refcount hits zero. - **SQLite is the metadata brain** (`better-sqlite3`, WAL mode): tables `nodes` (id, parent_id, name, type file/folder, blob_sha, size, mime, created, modified, starred, color, trashed_at), `shares`, `tags`, `node_tags`, `sessions`, `activity`, plus an **FTS5** virtual table for search. - **Everything streams.** Uploads and downloads never buffer whole files in memory. Range requests supported everywhere (video seeking!). - **Preview/transcode work is queued** (tiny in-process job queue, concurrency 2) and cached in `/srv/drive/cache/` keyed by blob sha — thumbnails and conversions are computed once per unique file, ever. ### 2.1 Repository layout of *this* project ``` spbdrive/ ├── CLAUDE.md ├── README.md # with badges ├── package.json ├── src/ │ ├── server.mjs # Fastify bootstrap │ ├── config.mjs # zod-validated env/config │ ├── db/ │ │ ├── schema.sql # full schema + FTS5 + indexes │ │ └── db.mjs # migrations, prepared statements │ ├── auth/ │ │ ├── password.mjs # argon2id verify/change, lockout │ │ └── session.mjs # cookie sessions (httpOnly, SameSite=Lax) │ ├── storage/ │ │ ├── blobs.mjs # CAS store: put(stream)→sha, get, refcount GC │ │ ├── nodes.mjs # tree ops: mkdir, move, copy, rename, trash │ │ └── upload.mjs # chunked/resumable upload endpoint │ ├── preview/ │ │ ├── router.mjs # mime → preview strategy dispatcher │ │ ├── thumbs.mjs # sharp: image thumbs; ffmpeg: video poster │ │ ├── transcode.mjs # ffmpeg audio/video web-safe transcodes │ │ ├── office.mjs # libreoffice --headless → PDF conversion │ │ ├── code.mjs # shiki highlighting + markdown-it (GFM) │ │ └── archive.mjs # zip/tar listing (and inner-file preview) │ ├── shares/ │ │ └── shares.mjs # tokens, expiry, passwords, limits, zip-of-folder │ ├── search/ │ │ └── search.mjs # FTS5 queries + filters │ ├── api/v1.mjs │ └── web/ │ ├── routes.mjs │ ├── views/ # Nunjucks templates (login, app shell, share) │ └── assets/ # tokens.css, app.css, app.js, fonts, icons ├── cli/ │ ├── spbdrive.mjs # bin entry │ └── commands/ ├── deploy/ │ ├── ngrok.yml │ ├── ecosystem.config.cjs # pm2: server + tunnel │ ├── spbdrive.service # systemd alternative │ ├── setup-m3u96b.sh # idempotent bootstrap (installs ffmpeg, libreoffice) │ └── backup.sh ├── scripts/ │ ├── inject-headers.mjs │ ├── check-headers.mjs │ └── reset-password.mjs └── test/ # vitest unit + e2e (upload→preview→share round-trip) ``` --- ## 3. Authentication & Sessions - **Login page** at `/login`: centered card, SPB monogram, single password field, "Remember me for 30 days" checkbox. Sharp, minimal, dark. - Verify with argon2id. **Brute-force protection**: 5 failed attempts → 15-minute lockout per IP + global exponential backoff; every failure logged to `activity`. - Session = random 256-bit id in `sessions` table; cookie `spbdrive_sid`, `httpOnly`, `Secure`, `SameSite=Lax`; 24 h TTL (30 d with remember-me), sliding renewal. - Every route except `/login`, `/s/*` (shares), `/healthz`, and static assets requires a valid session → otherwise 302 to `/login?next=...`. - Settings page: change password, view/revoke active sessions (device + last seen), toggle theme default. --- ## 4. File Management Core ### 4.1 Uploads (must feel instant and bulletproof) - **Drag & drop anywhere** in the app (full-window drop overlay), plus an Upload button (files *and* folders — use `webkitdirectory` and DataTransferItem tree walking so entire folder structures upload with hierarchy preserved). - **Chunked + resumable**: client slices files into 8 MB chunks, `POST /api/v1/upload/init` → uploadId, `PUT /api/v1/upload/:id/chunk/:n`, `POST /api/v1/upload/:id/complete` (server assembles, sha256-streams into blob store, creates node). Interrupted uploads resume by asking the server which chunks it has. - Upload panel (bottom-right, Google-Drive-style): per-file progress bars, speed, cancel, retry, aggregate progress, minimize. - Paste-to-upload (Ctrl+V an image/screenshot into the app → lands in the current folder as `pasted-YYYYMMDD-HHmmss.png`). - No file-size limit by design; test with a multi-GB file. ### 4.2 Folder tree & organization ("groupe folder etc.") - Unlimited nested folders. Left sidebar: collapsible folder tree + quick sections **Recent**, **Starred**, **Shared**, **Trash**, per-tag views, and a storage usage meter (used space, per-type breakdown donut). - **Folder colors** (8 palette choices) and **emoji/icon** per folder. - **Tags/labels**: create colored tags, assign to any file/folder, filter by tag. - **Starred/favorites** (toggle with `s`). - Operations, all with multi-select (click, shift-click ranges, ctrl-click, drag-rectangle select): **move (drag & drop onto folders or via dialog with tree picker), copy, rename (F2, inline), delete → Trash, restore, delete forever, download (multi-select → server-zips on the fly), duplicate**. - **Trash**: soft delete with original-path memory; auto-purge after 30 days (daily job); "Empty trash" with typed confirmation. - Breadcrumb path with drag-onto-crumb to move; right-click **context menu** everywhere (custom-rendered, keyboard accessible). - Conflict handling on move/upload: "Keep both (name-2)", "Replace", "Skip" — batch-applicable. ### 4.3 Views - **Grid view** (thumbnail cards, 5 sizes via slider) and **List view** (name, size, type, modified, tags) — toggle persisted per folder. - Sort: name / size / modified / type, asc-desc. Folders always first. - **Keyboard-first**: arrows navigate, Enter opens, Space = quick-look preview overlay, Del = trash, Ctrl+A select all, `/` focuses search, `?` shows a shortcuts cheat-sheet modal. --- ## 5. Universal Preview Engine — "all types imaginable" (Critical) Clicking a file opens the **Preview overlay**: full-screen modal, dark scrim, filename + size header, actions (Download · Share · Star · Info · Delete), ←/→ arrows to flip through siblings, Esc closes. Every strategy below is dispatched by `preview/router.mjs` from MIME + extension: | Category | Formats | Preview behavior | |---|---|---| | **Images** | jpg, png, gif, webp, avif, svg, bmp, ico, heic* | Zoom (wheel/pinch), pan, rotate, 1:1 toggle, EXIF panel (dimensions, camera, date, GPS→"open map" link). HEIC converted to jpg via sharp if supported, else download card. Animated gif/webp play. SVG sandboxed (served with strict CSP, no scripts). | | **Video** | mp4, webm, mov, mkv, avi | Native `