diff --git a/scripts/backup/engine/borg_backup.sh b/scripts/backup/engine/borg_backup.sh index d318b85..f2029a0 100644 --- a/scripts/backup/engine/borg_backup.sh +++ b/scripts/backup/engine/borg_backup.sh @@ -50,3 +50,44 @@ borgBackupAppToLocation() borgEnvUnset return $rc } + +borgBackupSystemToLocation() +{ + local idx="$1" + local source_path="${configs_dir%/}" + + if [[ ! -d "$source_path" ]]; then + isError "System config path missing: $source_path" + return 1 + fi + + borgEnvExport "$idx" || return 1 + + local host_tag="${CFG_INSTALL_NAME:-libreportal}" + # No app is named "system", so the "system--*" archive namespace can't + # collide with an app's archives. + local archive + archive=$(borgArchiveName "system" "$host_tag") + local comment="system=config host=$host_tag engine=libreportal" + + local loc_name + loc_name=$(resticLocationName "$idx") + isNotice "Snapshotting system config → $loc_name (archive: $archive)" >&2 + + runBackupOp borg create \ + --comment "$comment" \ + --compression auto,zstd \ + "::$archive" \ + "$source_path" + local rc=$? + + if [[ $rc -eq 0 ]]; then + isSuccessful "System config backed up to $loc_name: $archive" >&2 + echo "$archive" + else + isError "System config backup to $loc_name failed" >&2 + fi + + borgEnvUnset + return $rc +} diff --git a/scripts/backup/engine/borg_forget.sh b/scripts/backup/engine/borg_forget.sh index 5ec7d1e..bdcadcc 100644 --- a/scripts/backup/engine/borg_forget.sh +++ b/scripts/backup/engine/borg_forget.sh @@ -38,3 +38,41 @@ borgForgetApp() borgEnvUnset return $rc } + +borgForgetSystem() +{ + local idx="$1" + + if resticLocationAppendOnly "$idx"; then + isNotice "$(resticLocationName "$idx") is append-only — skipping forget for system config" + return 0 + fi + + local keep_last keep_daily keep_weekly keep_monthly keep_yearly + keep_last=$(resticRetentionFor "$idx" KEEP_LAST) + keep_daily=$(resticRetentionFor "$idx" KEEP_DAILY) + keep_weekly=$(resticRetentionFor "$idx" KEEP_WEEKLY) + keep_monthly=$(resticRetentionFor "$idx" KEEP_MONTHLY) + keep_yearly=$(resticRetentionFor "$idx" KEEP_YEARLY) + + borgEnvExport "$idx" || return 1 + + local host_tag="${CFG_INSTALL_NAME:-libreportal}" + local args=(prune --glob-archives "system-${host_tag}-*") + [[ -n "$keep_last" ]] && args+=(--keep-last "$keep_last") + [[ -n "$keep_daily" ]] && args+=(--keep-daily "$keep_daily") + [[ -n "$keep_weekly" ]] && args+=(--keep-weekly "$keep_weekly") + [[ -n "$keep_monthly" ]] && args+=(--keep-monthly "$keep_monthly") + [[ -n "$keep_yearly" ]] && args+=(--keep-yearly "$keep_yearly") + + isNotice "Applying retention for system config on $(resticLocationName "$idx")" + runBackupOp borg "${args[@]}" + local rc=$? + + if [[ "$CFG_BACKUP_PRUNE_AFTER_FORGET" == "true" && $rc -eq 0 ]]; then + runBackupOp borg compact + fi + + borgEnvUnset + return $rc +} diff --git a/scripts/backup/engine/borg_restore.sh b/scripts/backup/engine/borg_restore.sh index 31bd77b..e5f91b4 100644 --- a/scripts/backup/engine/borg_restore.sh +++ b/scripts/backup/engine/borg_restore.sh @@ -29,6 +29,26 @@ borgRestoreSnapshot() return $rc } +borgRestoreSystemLatest() +{ + local idx="$1" + local target_dir="$2" + local host="${3:-$CFG_INSTALL_NAME}" + + borgEnvExport "$idx" || return 1 + local snapshot_id + snapshot_id=$(runBackupOp borg list --json --glob-archives "system-${host}-*" --last 1 2>/dev/null \ + | grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4) + borgEnvUnset + + if [[ -z "$snapshot_id" ]]; then + isError "No system-config archive found in $(resticLocationName "$idx") for host=$host" + return 1 + fi + # Whole-archive extract into staging (no include subpath). + borgRestoreSnapshot "$idx" "$snapshot_id" "$target_dir" +} + borgDumpFile() { local idx="$1" diff --git a/scripts/backup/engine/borg_snapshots.sh b/scripts/backup/engine/borg_snapshots.sh index 23c6068..2f2e2ca 100644 --- a/scripts/backup/engine/borg_snapshots.sh +++ b/scripts/backup/engine/borg_snapshots.sh @@ -55,6 +55,50 @@ print(json.dumps(out)) fi } +borgSystemSnapshotsJson() +{ + local idx="$1" + local host_filter="$2" + + borgEnvExport "$idx" || return 1 + + local glob="system-${host_filter:-*}-*" + local raw + raw=$(runBackupOp borg list --json --glob-archives "$glob" 2>/dev/null) + local rc=$? + borgEnvUnset + [[ $rc -ne 0 || -z "$raw" ]] && { echo "[]"; return $rc; } + + if command -v jq >/dev/null 2>&1; then + echo "$raw" | jq -c '[.archives[] | { + id: .id, + short_id: .name, + time: .time, + hostname: .hostname, + tags: (.comment | split(" ") | map(select(test("^[a-z_]+=")))), + paths: [] + }]' + else + echo "$raw" | python3 -c ' +import json, sys +raw = json.load(sys.stdin) +out = [] +for a in raw.get("archives", []): + comment = a.get("comment", "") + tags = [t for t in comment.split() if "=" in t] + out.append({ + "id": a.get("id"), + "short_id": a.get("name"), + "time": a.get("time"), + "hostname": a.get("hostname"), + "tags": tags, + "paths": [] + }) +print(json.dumps(out)) +' 2>/dev/null || echo "[]" + fi +} + borgSnapshotLatestId() { local idx="$1" diff --git a/scripts/backup/engine/kopia_backup.sh b/scripts/backup/engine/kopia_backup.sh index f414cb6..d62c3cf 100644 --- a/scripts/backup/engine/kopia_backup.sh +++ b/scripts/backup/engine/kopia_backup.sh @@ -60,3 +60,41 @@ kopiaBackupAppToLocation() kopiaEnvUnset return $rc } + +kopiaBackupSystemToLocation() +{ + local idx="$1" + local source_path="${configs_dir%/}" + + if [[ ! -d "$source_path" ]]; then + isError "System config path missing: $source_path" + return 1 + fi + + kopiaEnvExport "$idx" || return 1 + + local host_tag="${CFG_INSTALL_NAME:-libreportal}" + local tags=("--tags" "system:config" "--tags" "host:$host_tag" "--tags" "engine:libreportal") + + local loc_name + loc_name=$(resticLocationName "$idx") + isNotice "Snapshotting system config → $loc_name (kopia)" >&2 + + local output + output=$(runBackupOp kopia snapshot create "$source_path" "${tags[@]}" --json 2>&1) + local rc=$? + + local snapshot_id + snapshot_id=$(echo "$output" | grep -oE '"id":\s*"[^"]+"' | head -1 | cut -d'"' -f4) + + if [[ $rc -eq 0 ]]; then + isSuccessful "System config backed up to $loc_name: ${snapshot_id:0:12}" >&2 + echo "$snapshot_id" + else + isError "Kopia system config backup to $loc_name failed" >&2 + echo "$output" | tail -10 >&2 + fi + + kopiaEnvUnset + return $rc +} diff --git a/scripts/backup/engine/kopia_forget.sh b/scripts/backup/engine/kopia_forget.sh index 5aa8e86..1eb9f27 100644 --- a/scripts/backup/engine/kopia_forget.sh +++ b/scripts/backup/engine/kopia_forget.sh @@ -41,3 +41,39 @@ kopiaForgetApp() kopiaEnvUnset return $rc } + +kopiaForgetSystem() +{ + local idx="$1" + + if resticLocationAppendOnly "$idx"; then + isNotice "$(resticLocationName "$idx") is append-only — skipping forget for system config" + return 0 + fi + + local keep_last keep_daily keep_weekly keep_monthly keep_yearly + keep_last=$(resticRetentionFor "$idx" KEEP_LAST) + keep_daily=$(resticRetentionFor "$idx" KEEP_DAILY) + keep_weekly=$(resticRetentionFor "$idx" KEEP_WEEKLY) + keep_monthly=$(resticRetentionFor "$idx" KEEP_MONTHLY) + keep_yearly=$(resticRetentionFor "$idx" KEEP_YEARLY) + + kopiaEnvExport "$idx" || return 1 + + local src="${configs_dir%/}" + + local policy_args=(policy set --global=false "$src") + [[ -n "$keep_last" ]] && policy_args+=(--keep-latest "$keep_last") + [[ -n "$keep_daily" ]] && policy_args+=(--keep-daily "$keep_daily") + [[ -n "$keep_weekly" ]] && policy_args+=(--keep-weekly "$keep_weekly") + [[ -n "$keep_monthly" ]] && policy_args+=(--keep-monthly "$keep_monthly") + [[ -n "$keep_yearly" ]] && policy_args+=(--keep-annual "$keep_yearly") + + runBackupOp kopia "${policy_args[@]}" >/dev/null 2>&1 + + isNotice "Running Kopia maintenance for system config on $(resticLocationName "$idx")" + runBackupOp kopia maintenance run --full + local rc=$? + kopiaEnvUnset + return $rc +} diff --git a/scripts/backup/engine/kopia_snapshots.sh b/scripts/backup/engine/kopia_snapshots.sh index 2efd8b0..ea248b5 100644 --- a/scripts/backup/engine/kopia_snapshots.sh +++ b/scripts/backup/engine/kopia_snapshots.sh @@ -42,6 +42,61 @@ kopiaSnapshotsJson() fi } +kopiaSystemSnapshotsJson() +{ + local idx="$1" + local host_filter="$2" + + kopiaEnvExport "$idx" || return 1 + + local raw + raw=$(runBackupOp kopia snapshot list --all --json 2>/dev/null) + local rc=$? + kopiaEnvUnset + [[ $rc -ne 0 || -z "$raw" ]] && { echo "[]"; return $rc; } + + local jq_filter='[.[] | { + id: .id, + short_id: (.id[0:8]), + time: .startTime, + hostname: .source.host, + tags: ((.tags // []) | map(sub(":"; "="))), + paths: [.source.path] + }]' + jq_filter='[.[] | select(any(.tags[]?; . == "system:config"))] | '"$jq_filter" + if [[ -n "$host_filter" ]]; then + jq_filter='[.[] | select(.source.host == "'"$host_filter"'")] | '"$jq_filter" + fi + + if command -v jq >/dev/null 2>&1; then + echo "$raw" | jq -c "$jq_filter" + else + echo "$raw" + fi +} + +kopiaRestoreSystemLatest() +{ + local idx="$1" + local target_dir="$2" + local host="${3:-$CFG_INSTALL_NAME}" + + local json snapshot_id + json=$(kopiaSystemSnapshotsJson "$idx" "$host") + if command -v jq >/dev/null 2>&1; then + snapshot_id=$(echo "$json" | jq -r 'sort_by(.time) | last | .id // empty') + else + snapshot_id=$(echo "$json" | grep -oE '"id":\s*"[^"]+"' | tail -1 | cut -d'"' -f4) + fi + + if [[ -z "$snapshot_id" ]]; then + isError "No system-config snapshot found in $(resticLocationName "$idx") for host=$host" + return 1 + fi + # Whole-snapshot restore into staging (no include subpath). + kopiaRestoreSnapshot "$idx" "$snapshot_id" "$target_dir" +} + kopiaSnapshotLatestId() { local idx="$1"