#!/usr/bin/env bash set -euo pipefail # Backs up key OpenClaw state to a versioned ZIP archive. # Includes: # - ~/.openclaw/openclaw.json # - ~/.openclaw/cron/ # - ~/.openclaw/credentials/ # - ~/.openclaw/delivery-queue/ (if present) # # Usage: # scripts/backup-openclaw-state.sh [output_dir] # Example: # scripts/backup-openclaw-state.sh /home/node/.openclaw/backups OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}" OUTPUT_DIR="${1:-$OPENCLAW_HOME/backups}" TS="$(date -u +"%Y%m%d-%H%M%SZ")" mkdir -p "$OUTPUT_DIR" get_version() { local raw ver raw="$(openclaw --version 2>/dev/null || true)" if [[ -n "$raw" ]]; then ver="$(printf '%s' "$raw" | head -n1 | sed -E 's/[^0-9A-Za-z._-]+/-/g; s/^-+|-+$//g')" if [[ -n "$ver" ]]; then printf '%s' "$ver" return 0 fi fi raw="$(openclaw status 2>/dev/null | grep -E 'Channel|Update' | head -n1 || true)" ver="$(printf '%s' "$raw" | sed -E 's/[^0-9A-Za-z._-]+/-/g; s/^-+|-+$//g')" if [[ -n "$ver" ]]; then printf '%s' "$ver" else printf 'unknown' fi } VERSION="$(get_version)" ARCHIVE_NAME="openclaw-backup-${TS}-v${VERSION}.zip" ARCHIVE_PATH="$OUTPUT_DIR/$ARCHIVE_NAME" # Build include list (required + optional) INCLUDE_PATHS=() if [[ -f "$OPENCLAW_HOME/openclaw.json" ]]; then INCLUDE_PATHS+=("$OPENCLAW_HOME/openclaw.json") else echo "WARN: Missing $OPENCLAW_HOME/openclaw.json" >&2 fi if [[ -d "$OPENCLAW_HOME/cron" ]]; then INCLUDE_PATHS+=("$OPENCLAW_HOME/cron") else echo "WARN: Missing $OPENCLAW_HOME/cron" >&2 fi if [[ -d "$OPENCLAW_HOME/credentials" ]]; then INCLUDE_PATHS+=("$OPENCLAW_HOME/credentials") else echo "WARN: Missing $OPENCLAW_HOME/credentials" >&2 fi if [[ -d "$OPENCLAW_HOME/delivery-queue" ]]; then INCLUDE_PATHS+=("$OPENCLAW_HOME/delivery-queue") fi if [[ ${#INCLUDE_PATHS[@]} -eq 0 ]]; then echo "ERROR: Nothing to back up." >&2 exit 1 fi # Create a temp metadata file and include it in the archive. TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT META_FILE="$TMP_DIR/backup-metadata.txt" { echo "timestamp_utc=$TS" echo "openclaw_version=$VERSION" echo "openclaw_home=$OPENCLAW_HOME" echo "archive_name=$ARCHIVE_NAME" echo "included_paths=" for p in "${INCLUDE_PATHS[@]}"; do echo " - $p" done } > "$META_FILE" # Create zip via Python for consistent behavior. python3 - "$ARCHIVE_PATH" "$META_FILE" "${INCLUDE_PATHS[@]}" <<'PY' import os import sys import zipfile from pathlib import Path archive = Path(sys.argv[1]) meta_file = Path(sys.argv[2]) items = [Path(p) for p in sys.argv[3:]] with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED) as zf: # Put metadata at archive root zf.write(meta_file, arcname="backup-metadata.txt") for item in items: if not item.exists(): continue if item.is_file(): zf.write(item, arcname=str(item).lstrip("/")) else: for root, dirs, files in os.walk(item): root_path = Path(root) # preserve empty dirs if not files and not dirs: zi = zipfile.ZipInfo(str(root_path).lstrip("/") + "/") zf.writestr(zi, "") for f in files: fp = root_path / f zf.write(fp, arcname=str(fp).lstrip("/")) PY echo "Backup created: $ARCHIVE_PATH"