84 lines
3.0 KiB
Bash
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
|