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:
70
backup.sh
Executable file
70
backup.sh
Executable file
@@ -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
|
||||
79
install.py
79
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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user