#!/usr/bin/env bash set -euo pipefail BIN_ROOT="/opt/selective-vpn/bin" COMPONENTS_RAW="" DRY_RUN=0 usage() { cat <<'EOF' Usage: rollback.sh [--bin-root DIR] [--component NAME[,NAME...]] [--dry-run] Description: Rolls back transport companion binaries by one history step. Uses history files created by update.sh in BIN_ROOT/.packaging/*.history. Examples: ./scripts/transport-packaging/rollback.sh --component singbox ./scripts/transport-packaging/rollback.sh --bin-root /tmp/svpn-bin --dry-run EOF } require_cmd() { local cmd="$1" if ! command -v "$cmd" >/dev/null 2>&1; then echo "[transport-rollback] missing required command: ${cmd}" >&2 exit 1 fi } collect_components() { local state_dir="$1" local out=() if [[ -n "$COMPONENTS_RAW" ]]; then IFS=',' read -r -a out <<< "$COMPONENTS_RAW" else if [[ -d "$state_dir" ]]; then while IFS= read -r file; do local base base="$(basename "$file")" out+=("${base%.history}") done < <(find "$state_dir" -maxdepth 1 -type f -name '*.history' | sort) fi fi printf '%s\n' "${out[@]}" } rollback_component() { local component="$1" local state_dir="$2" local history_file="${state_dir}/${component}.history" if [[ ! -f "$history_file" ]]; then echo "[transport-rollback] ${component}: history file not found (${history_file})" >&2 return 1 fi mapfile -t lines < "$history_file" if [[ "${#lines[@]}" -lt 2 ]]; then echo "[transport-rollback] ${component}: not enough history entries to rollback" >&2 return 1 fi local current_line prev_line current_line="${lines[${#lines[@]}-1]}" prev_line="${lines[${#lines[@]}-2]}" IFS='|' read -r _ts_curr bin_curr version_curr target_curr <<< "$current_line" IFS='|' read -r _ts_prev bin_prev version_prev target_prev <<< "$prev_line" local binary_name="${bin_curr:-$bin_prev}" local active_link="${BIN_ROOT}/${binary_name}" if [[ -z "$binary_name" || -z "$target_prev" ]]; then echo "[transport-rollback] ${component}: invalid history lines" >&2 return 1 fi if [[ ! -e "$target_prev" ]]; then echo "[transport-rollback] ${component}: previous target does not exist: ${target_prev}" >&2 return 1 fi echo "[transport-rollback] ${component}: ${binary_name} ${version_curr} -> ${version_prev}" if [[ "$DRY_RUN" -eq 1 ]]; then echo "[transport-rollback] DRY-RUN ${component}: switch ${active_link} -> ${target_prev}" return 0 fi ln -sfn "$target_prev" "$active_link" if [[ "${#lines[@]}" -eq 2 ]]; then printf '%s\n' "${lines[0]}" > "$history_file" else printf '%s\n' "${lines[@]:0:${#lines[@]}-1}" > "$history_file" fi echo "[transport-rollback] ${component}: active -> ${target_prev}" } while [[ $# -gt 0 ]]; do case "$1" in --bin-root) BIN_ROOT="${2:-}" shift 2 ;; --component) COMPONENTS_RAW="${2:-}" shift 2 ;; --dry-run) DRY_RUN=1 shift ;; -h|--help) usage exit 0 ;; *) echo "[transport-rollback] unknown argument: $1" >&2 usage >&2 exit 1 ;; esac done require_cmd ln require_cmd find require_cmd basename state_dir="${BIN_ROOT}/.packaging" mapfile -t components < <(collect_components "$state_dir") if [[ "${#components[@]}" -eq 0 ]]; then echo "[transport-rollback] no components selected/found" >&2 exit 1 fi echo "[transport-rollback] bin_root=${BIN_ROOT}" if [[ -n "$COMPONENTS_RAW" ]]; then echo "[transport-rollback] components=${COMPONENTS_RAW}" fi if [[ "$DRY_RUN" -eq 1 ]]; then echo "[transport-rollback] mode=dry-run" fi for component in "${components[@]}"; do component="$(echo "$component" | xargs)" if [[ -z "$component" ]]; then continue fi rollback_component "$component" "$state_dir" done echo "[transport-rollback] done"