diff --git a/backup.sh b/backup.sh new file mode 100755 index 0000000..e609b24 --- /dev/null +++ b/backup.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# backup.sh — restic backup of Cortex persona/home data +# +# Backs up the home/ directory (all user persona files, memory, tasks, crons). +# Code is in git; this covers everything git intentionally excludes. +# +# Config — override via environment or edit here: +REPO_DIR="${RESTIC_REPOSITORY:-$HOME/backups/cortex-home-restic}" +PASSWORD_FILE="${RESTIC_PASSWORD_FILE:-$HOME/.config/cortex/restic-password}" +SOURCE="$(cd "$(dirname "$0")" && pwd)/home" + +# Retention policy +KEEP_DAILY=7 +KEEP_WEEKLY=4 +KEEP_MONTHLY=6 + +# ── Preflight ───────────────────────────────────────────────────────────────── + +set -euo pipefail + +if ! command -v restic &>/dev/null; then + echo "ERROR: restic not found. Install with: sudo pacman -S restic" >&2 + exit 1 +fi + +if [[ ! -d "$SOURCE" ]]; then + echo "ERROR: source directory not found: $SOURCE" >&2 + exit 1 +fi + +# ── Password setup ──────────────────────────────────────────────────────────── + +if [[ ! -f "$PASSWORD_FILE" ]]; then + mkdir -p "$(dirname "$PASSWORD_FILE")" + chmod 700 "$(dirname "$PASSWORD_FILE")" + python3 -c "import secrets; print(secrets.token_urlsafe(32))" > "$PASSWORD_FILE" + chmod 600 "$PASSWORD_FILE" + echo "Generated new restic password: $PASSWORD_FILE" + echo "IMPORTANT: back this file up separately — you need it to restore." +fi + +export RESTIC_REPOSITORY="$REPO_DIR" +export RESTIC_PASSWORD_FILE="$PASSWORD_FILE" + +# ── Init repo if needed ─────────────────────────────────────────────────────── + +if [[ ! -d "$REPO_DIR" ]]; then + echo "Initializing restic repository at $REPO_DIR …" + restic init +fi + +# ── Backup ──────────────────────────────────────────────────────────────────── + +echo "Backing up $SOURCE → $REPO_DIR" +restic backup "$SOURCE" \ + --exclude="**/sessions" \ + --exclude="**/session_data" \ + --tag "cortex-home" + +# ── Prune ───────────────────────────────────────────────────────────────────── + +restic forget \ + --keep-daily "$KEEP_DAILY" \ + --keep-weekly "$KEEP_WEEKLY" \ + --keep-monthly "$KEEP_MONTHLY" \ + --tag "cortex-home" \ + --prune + +echo "Backup complete." +restic snapshots --tag cortex-home --last 3 diff --git a/install.py b/install.py index 2a9f92d..1e1adf0 100755 --- a/install.py +++ b/install.py @@ -32,6 +32,10 @@ SERVICE_FILE = Path.home() / ".config" / "systemd" / "user" / f"{SERVICE_NAME}.s CLAUDE_CREDS = Path.home() / ".claude" / ".credentials.json" GEMINI_CREDS = Path.home() / ".gemini" / "oauth_creds.json" +BACKUP_SCRIPT = PROJECT_ROOT / "backup.sh" +BACKUP_SVC_FILE = Path.home() / ".config" / "systemd" / "user" / "cortex-backup.service" +BACKUP_TMR_FILE = Path.home() / ".config" / "systemd" / "user" / "cortex-backup.timer" + # ── ANSI helpers ────────────────────────────────────────────────────────────── RESET = "\033[0m" @@ -252,6 +256,72 @@ def check_cli_auth(): warn(" (completes an OAuth flow in your browser)") +def setup_backup_timer(dry_run=False): + header("Backup (restic systemd timer)") + + code, _, _ = run_quiet(["which", "restic"]) + if code != 0: + warn("restic not found — skipping backup timer setup") + warn(" Install with: sudo pacman -S restic (or your distro's package manager)") + return + + ok("restic found") + + svc_content = f"""\ +[Unit] +Description=Cortex home backup (restic) +After=network.target + +[Service] +Type=oneshot +ExecStart={BACKUP_SCRIPT} +""" + + tmr_content = """\ +[Unit] +Description=Cortex home backup — daily at 03:00 + +[Timer] +OnCalendar=*-*-* 03:00:00 +Persistent=true + +[Install] +WantedBy=timers.target +""" + + if dry_run: + if BACKUP_SVC_FILE.exists() and BACKUP_TMR_FILE.exists(): + ok(f"Timer already installed") + code2, out, _ = run_quiet(["systemctl", "--user", "is-enabled", + "cortex-backup.timer"]) + ok(f"Timer enabled: {code2 == 0}") + else: + warn("Backup timer not installed — would write service + timer units") + return + + SERVICE_FILE.parent.mkdir(parents=True, exist_ok=True) + + for path, content, label in [ + (BACKUP_SVC_FILE, svc_content, "service"), + (BACKUP_TMR_FILE, tmr_content, "timer"), + ]: + existing = path.read_text() if path.exists() else "" + if existing == content: + ok(f"Backup {label} file unchanged") + else: + path.write_text(content) + ok(f"Backup {label} file written: {path}") + + run(["systemctl", "--user", "daemon-reload"]) + run(["systemctl", "--user", "enable", "--now", "cortex-backup.timer"]) + ok("Backup timer enabled (daily 03:00)") + + code2, out, _ = run_quiet(["systemctl", "--user", "list-timers", + "cortex-backup.timer", "--no-pager"]) + for line in out.splitlines()[:4]: + info(line) + + def print_status(): header("Service status") code, out, _ = run_quiet(["systemctl", "--user", "status", SERVICE_NAME, @@ -287,13 +357,18 @@ def main(): if not dry: manage_service(dry_run=dry) check_cli_auth() + setup_backup_timer(dry_run=dry) if not dry: print_status() print(f"\n{BOLD}Done.{RESET}\n") - print(" Logs: journalctl --user -u cortex -f") - print(" Swagger: http://localhost:8000/docs") + print(" Logs: journalctl --user -u cortex -f") + print(" Swagger: http://localhost:8000/docs") + print(" Backup: ./backup.sh (also runs daily at 03:00)") + print(" Snapshots: RESTIC_REPOSITORY=~/backups/cortex-home-restic \\") + print(" RESTIC_PASSWORD_FILE=~/.config/cortex/restic-password \\") + print(" restic snapshots") print()