#!/usr/bin/env bash # ───────────────────────────────────────────── # SPB Drive — Personal Cloud Drive # ───────────────────────────────────────────── # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # File : deploy/setup-m3u96b.sh # Purpose : Idempotent bootstrap of SPB Drive on node m3u96b (macOS) # License : MIT © Simon-Pierre Boucher # ───────────────────────────────────────────── # # Usage (on m3u96b, from anywhere): # SPBDRIVE_BOOTSTRAP_PASSWORD=… bash deploy/setup-m3u96b.sh [path-to-repo] # The repo is rsynced (or already present) at /srv/drive/app. # Re-running is always safe. set -euo pipefail # macOS seals /srv (read-only system volume) — the drive lives under $HOME/srv. DATA_DIR="$HOME/srv/drive" APP_DIR="$DATA_DIR/app" REPO_SRC="${1:-$PWD}" BREW="$(command -v brew || echo /opt/homebrew/bin/brew)" log() { printf '\033[1;34m▸ %s\033[0m\n' "$*"; } # 1 ── System packages ────────────────────────────────────────────────── log "Installing system packages (ffmpeg, libreoffice, poppler, 7z, ngrok, pm2)" "$BREW" list ffmpeg >/dev/null 2>&1 || "$BREW" install ffmpeg "$BREW" list poppler >/dev/null 2>&1 || "$BREW" install poppler # pdftotext + pdftoppm "$BREW" list sevenzip >/dev/null 2>&1 || "$BREW" install sevenzip # 7z if [ ! -d /Applications/LibreOffice.app ]; then # macOS 26 blocks headless hdiutil attach with a consent prompt, so the cask # can't install over SSH. Fetch the DMG via brew, extract it with 7zz instead. "$BREW" fetch --cask libreoffice >/dev/null || true LO_DMG="$(ls "$HOME"/Library/Caches/Homebrew/downloads/*LibreOffice*.dmg 2>/dev/null | head -1)" if [ -n "$LO_DMG" ]; then LO_TMP="$(mktemp -d)" (cd "$LO_TMP" && 7zz x -y "$LO_DMG" >/dev/null 2>&1) || true LO_APP="$(find "$LO_TMP" -maxdepth 3 -name 'LibreOffice.app' | head -1)" if [ -n "$LO_APP" ]; then ditto "$LO_APP" /Applications/LibreOffice.app xattr -dr com.apple.quarantine /Applications/LibreOffice.app 2>/dev/null || true fi rm -rf "$LO_TMP" fi [ -d /Applications/LibreOffice.app ] || \ echo "⚠ LibreOffice install failed — office previews will fall back to download cards" >&2 fi command -v ngrok >/dev/null 2>&1 || "$BREW" install ngrok command -v node >/dev/null 2>&1 || "$BREW" install node@20 command -v pm2 >/dev/null 2>&1 || npm install -g pm2 NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')" if [ "$NODE_MAJOR" -lt 20 ]; then echo "Node >= 20 required (found $(node -v))" >&2 exit 1 fi # 2 ── Data directories (700) ─────────────────────────────────────────── log "Creating $DATA_DIR layout" mkdir -p "$DATA_DIR"/{files,db,cache,backups,logs} chmod 700 "$DATA_DIR" "$DATA_DIR"/{files,db,cache,backups,logs} # 3 ── App code + dependencies ────────────────────────────────────────── log "Syncing app to $APP_DIR" mkdir -p "$APP_DIR" if [ "$REPO_SRC" != "$APP_DIR" ]; then rsync -a --delete \ --exclude node_modules --exclude data --exclude .git --exclude .DS_Store \ "$REPO_SRC"/ "$APP_DIR"/ fi cd "$APP_DIR" log "Installing production dependencies" npm ci --omit=dev --no-audit --no-fund # 4 ── First-boot password + keys ─────────────────────────────────────── if [ ! -f "$DATA_DIR/auth.json" ]; then if [ -z "${SPBDRIVE_BOOTSTRAP_PASSWORD:-}" ]; then printf 'First boot — choose the drive password: ' read -rs SPBDRIVE_BOOTSTRAP_PASSWORD echo export SPBDRIVE_BOOTSTRAP_PASSWORD fi log "Seeding auth.json + keys.json (argon2id, chmod 600)" SPBDRIVE_DATA_DIR="$DATA_DIR" node -e ' import("./src/config.mjs").then(async ({ ensureDataDirs }) => { ensureDataDirs(); const { ensureAuthBootstrap } = await import("./src/auth/password.mjs"); await ensureAuthBootstrap(); console.log("auth seeded"); }); ' else log "auth.json already present — keeping existing password" fi # 5 ── pm2: server + tunnel, boot persistence ────────────────────────── log "Starting pm2 apps" pm2 startOrReload deploy/ecosystem.config.cjs pm2 save if ! pm2 startup 2>/dev/null | grep -q 'already'; then log "If pm2 printed a startup command above, run it once with sudo so the drive survives reboots." fi # 6 ── Health check ───────────────────────────────────────────────────── sleep 3 if curl -fsS http://127.0.0.1:7430/healthz >/dev/null; then log "✓ SPB Drive is healthy on :7430 → https://drive.spboucher.ai" else echo "✗ healthz failed — check: pm2 logs spbdrive-server" >&2 exit 1 fi # 7 ── Nightly backup cron (02:30) ────────────────────────────────────── log "Installing nightly backup cron" CRON_LINE="30 2 * * * /bin/bash $APP_DIR/deploy/backup.sh >> $DATA_DIR/logs/backup.log 2>&1" ( crontab -l 2>/dev/null | grep -v 'deploy/backup.sh' ; echo "$CRON_LINE" ) | crontab - log "Done. Reserve drive.spboucher.ai in the ngrok dashboard + DNS CNAME if not already done."