aipackage/scripts/cezen-backup.sh

84 lines
3.0 KiB
Bash

#!/usr/bin/env bash
# Nexus One AI backup/restore helper.
#
# Usage:
# sudo bash scripts/cezen-backup.sh backup
# sudo bash scripts/cezen-backup.sh list
# sudo bash scripts/cezen-backup.sh restore /opt/cezen/backups/cezen-backup-YYYYmmdd-HHMMSS.zip
set -euo pipefail
ACTION="${1:-backup}"
TARGET="${2:-}"
DATA_DIR="${CEZEN_DATA:-/opt/cezen/data}"
BACKUP_DIR="${CEZEN_BACKUP_DIR:-/opt/cezen/backups}"
python3 - "$ACTION" "$TARGET" "$DATA_DIR" "$BACKUP_DIR" <<'PY'
import json
import shutil
import sys
import zipfile
from datetime import datetime, timezone
from pathlib import Path
action, target, data_dir, backup_dir = sys.argv[1:5]
data_dir = Path(data_dir)
backup_dir = Path(backup_dir)
backup_dir.mkdir(parents=True, exist_ok=True)
def now_tag():
return datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
def iso_now():
return datetime.now(timezone.utc).isoformat()
def write_backup(dest):
manifest = {
"schema": "cezen.backup_manifest.v1",
"created_at": iso_now(),
"data_dir": str(data_dir),
"source": "cezen-backup.sh",
}
with zipfile.ZipFile(dest, "w", compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr("manifest.json", json.dumps(manifest, indent=2))
if data_dir.exists():
for path in data_dir.rglob("*"):
if path.is_file():
zf.write(path, path.relative_to(data_dir).as_posix())
def restore(src):
src = Path(src)
if not src.exists():
raise SystemExit(f"Backup not found: {src}")
safety = backup_dir / f"pre-restore-{now_tag()}.zip"
write_backup(safety)
root = data_dir.resolve()
with zipfile.ZipFile(src, "r") as zf:
for member in zf.infolist():
if member.filename == "manifest.json" or member.is_dir():
continue
target_path = (data_dir / member.filename).resolve()
if root not in target_path.parents and target_path != root:
raise SystemExit(f"Unsafe archive path: {member.filename}")
target_path.parent.mkdir(parents=True, exist_ok=True)
with zf.open(member) as source, open(target_path, "wb") as out:
shutil.copyfileobj(source, out)
print(json.dumps({"ok": True, "restored": str(src), "pre_restore_snapshot": str(safety)}, indent=2))
if action == "backup":
dest = backup_dir / f"cezen-backup-{now_tag()}.zip"
write_backup(dest)
print(json.dumps({"ok": True, "backup": str(dest)}, indent=2))
elif action == "list":
rows = []
for path in sorted(backup_dir.glob("cezen-backup-*.zip"), key=lambda p: p.stat().st_mtime, reverse=True):
rows.append({"name": path.name, "path": str(path), "size_bytes": path.stat().st_size})
print(json.dumps({"backup_dir": str(backup_dir), "backups": rows}, indent=2))
elif action == "restore":
if not target:
raise SystemExit("Usage: cezen-backup.sh restore /path/to/backup.zip")
restore(target)
else:
raise SystemExit("Usage: cezen-backup.sh backup|list|restore [backup.zip]")
PY