feat: restic backup of home/ with systemd daily timer

- backup.sh — backs up home/ (persona data, memory, tasks, crons) to a
  local restic repo; auto-generates password on first run, prunes to
  7d/4w/6m retention; excludes sessions/ and session_data/
- install.py — setup_backup_timer() installs cortex-backup.service +
  cortex-backup.timer (daily 03:00, Persistent=true); skips gracefully
  if restic is not installed

Password lives at ~/.config/cortex/restic-password (chmod 600, not in git).
Repo defaults to ~/backups/cortex-home-restic; override via RESTIC_REPOSITORY.

Per-user encrypted backup is a noted future feature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-08 19:36:22 -04:00
parent bf800acca8
commit 576f22216a
2 changed files with 147 additions and 2 deletions

View File

@@ -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()