421b802467
- Corrected the backup path in the `_in_backup` function to reflect the new structure under `mackup/`. - Improved error handling in `ssr::mackup::configure` to skip restore if the configuration file is not updated, providing user feedback. - Updated `ssr::sync` to conditionally execute backup based on configuration validity, enhancing robustness. - Revised documentation in configuration examples to clarify the new Mackup directory structure and its implications for users.
213 lines
7.7 KiB
Bash
213 lines
7.7 KiB
Bash
#!/usr/bin/env bash
|
|
# lib/common.sh — shared helpers (logging, errors, validation).
|
|
# Sourced by all ssr commands. Never executed directly.
|
|
|
|
set -euo pipefail
|
|
|
|
# Resolve repo root from any sourced location.
|
|
SSR_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SSR_ROOT="$(cd "${SSR_LIB_DIR}/.." && pwd)"
|
|
export SSR_ROOT SSR_LIB_DIR
|
|
|
|
# Colors only when stdout is a TTY (no ANSI in launchd logs).
|
|
if [[ -t 1 ]]; then
|
|
SSR_C_RED=$'\033[31m'; SSR_C_GRN=$'\033[32m'
|
|
SSR_C_YLW=$'\033[33m'; SSR_C_BLU=$'\033[34m'
|
|
SSR_C_DIM=$'\033[2m'; SSR_C_RST=$'\033[0m'
|
|
else
|
|
SSR_C_RED=""; SSR_C_GRN=""; SSR_C_YLW=""; SSR_C_BLU=""; SSR_C_DIM=""; SSR_C_RST=""
|
|
fi
|
|
|
|
ssr::ts() { date '+%Y-%m-%dT%H:%M:%S%z'; }
|
|
|
|
ssr::log() { printf '%s %s[INFO]%s %s\n' "$(ssr::ts)" "$SSR_C_BLU" "$SSR_C_RST" "$*"; }
|
|
ssr::ok() { printf '%s %s[OK]%s %s\n' "$(ssr::ts)" "$SSR_C_GRN" "$SSR_C_RST" "$*"; }
|
|
ssr::warn() { printf '%s %s[WARN]%s %s\n' "$(ssr::ts)" "$SSR_C_YLW" "$SSR_C_RST" "$*" >&2; }
|
|
ssr::err() { printf '%s %s[ERR]%s %s\n' "$(ssr::ts)" "$SSR_C_RED" "$SSR_C_RST" "$*" >&2; }
|
|
ssr::die() { ssr::err "$@"; exit 1; }
|
|
|
|
# Mirror to syslog if available so launchd runs are findable via `log show`.
|
|
ssr::syslog() {
|
|
[[ -x /usr/bin/logger ]] || return 0
|
|
/usr/bin/logger -t "ssr" -- "$*" || true
|
|
}
|
|
|
|
ssr::require_macos() {
|
|
[[ "$(uname -s)" == "Darwin" ]] || ssr::die "ssr requires macOS (Darwin). Detected: $(uname -s)"
|
|
}
|
|
|
|
ssr::require_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || ssr::die "Required command missing: $1"
|
|
}
|
|
|
|
# Confirm prompt with optional timeout (returns 0 = yes).
|
|
ssr::confirm() {
|
|
local prompt="${1:-Continue?}" timeout="${2:-0}" reply
|
|
if [[ "$timeout" -gt 0 ]]; then
|
|
read -r -t "$timeout" -n 1 -p "${prompt} [y/N]: " reply || reply=""
|
|
else
|
|
read -r -n 1 -p "${prompt} [y/N]: " reply || reply=""
|
|
fi
|
|
echo
|
|
case "$reply" in y|Y) return 0 ;; *) return 1 ;; esac
|
|
}
|
|
|
|
# Install or replace $1 with the contents of regular file $2 (staging path).
|
|
# - Missing path or dangling symlink: writes immediately (no overwrite).
|
|
# - Same bytes as staging: no-op, return 0.
|
|
# - Differs: prints unified diff (existing → proposed) on stderr, then either
|
|
# prompts on a TTY or requires SSR_ALLOW_CONF_OVERWRITE=yes (non-interactive).
|
|
# Returns 0 after a successful write or no-op; 1 if the user declined or
|
|
# non-interactive without SSR_ALLOW_CONF_OVERWRITE.
|
|
ssr::replace_file_with_confirm() {
|
|
local target="$1" staging="$2"
|
|
ssr::validate_path "$target"
|
|
ssr::validate_path "$staging"
|
|
[[ -f "$staging" ]] || ssr::die "Staging file not found: $staging"
|
|
[[ -d "$target" ]] && ssr::die "Refusing to replace a directory: $target"
|
|
|
|
# Nothing there yet (or dangling symlink) — create without prompt.
|
|
if [[ ! -e "$target" ]]; then
|
|
rm -f "$target"
|
|
cat "$staging" > "$target"
|
|
ssr::ok "Created $target"
|
|
return 0
|
|
fi
|
|
|
|
if ! [[ -f "$target" ]]; then
|
|
ssr::die "Refusing to replace non-regular file: $target"
|
|
fi
|
|
|
|
if cmp -s "$target" "$staging" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
printf '\n' >&2
|
|
ssr::log "Proposed change to $target (unified diff: existing → proposed)"
|
|
diff -u "$target" "$staging" >&2 || true
|
|
printf '\n' >&2
|
|
|
|
case "${SSR_ALLOW_CONF_OVERWRITE:-}" in
|
|
yes|1|true|on)
|
|
ssr::log "SSR_ALLOW_CONF_OVERWRITE set — applying change to $target"
|
|
;;
|
|
*)
|
|
if ! [[ -t 0 ]]; then
|
|
ssr::warn "Refusing to overwrite $target (stdin is not a TTY)."
|
|
ssr::warn "Review the diff above, merge manually, or run once with:"
|
|
ssr::warn " SSR_ALLOW_CONF_OVERWRITE=yes ssr sync # or ssr restore"
|
|
return 1
|
|
fi
|
|
ssr::confirm "Replace $target as shown in the diff above?" 0 || {
|
|
ssr::warn "Leaving $target unchanged."
|
|
return 1
|
|
}
|
|
;;
|
|
esac
|
|
|
|
cp -f "$target" "$target.bak.$(date +%s)" 2>/dev/null || true
|
|
rm -f "$target"
|
|
cat "$staging" > "$target"
|
|
ssr::ok "Updated $target (previous saved as ${target}.bak.*)"
|
|
return 0
|
|
}
|
|
|
|
# Validate a path is safe (no newlines, no `..` traversal). Used before any FS op.
|
|
# Note: bash strings cannot hold NUL bytes, so no separate NUL check needed.
|
|
ssr::validate_path() {
|
|
local p="$1"
|
|
[[ -n "$p" ]] || ssr::die "Empty path"
|
|
case "$p" in
|
|
*$'\n'*) ssr::die "Path contains newline: $p" ;;
|
|
*..*) ssr::die "Path traversal rejected: $p" ;;
|
|
esac
|
|
}
|
|
|
|
ssr::ensure_dir() {
|
|
local d="$1"
|
|
ssr::validate_path "$d"
|
|
[[ -d "$d" ]] || mkdir -p "$d"
|
|
}
|
|
|
|
# If ssr.conf lists SSR_CLOUD_PROVIDER more than once, rewrite
|
|
# the file to a single assignment: same value repeated is merged silently;
|
|
# different values prompt on a TTY (stores choice) or abort without TTY.
|
|
ssr::config::resolve_cloud_provider_duplicates() {
|
|
local cfg="$1"
|
|
local -a vals=()
|
|
local line
|
|
while IFS= read -r line; do
|
|
[[ "$line" =~ ^SSR_CLOUD_PROVIDER=(.*)$ ]] || continue
|
|
vals+=("${BASH_REMATCH[1]}")
|
|
done < <(grep '^SSR_CLOUD_PROVIDER=' "$cfg" 2>/dev/null || true)
|
|
[[ ${#vals[@]} -le 1 ]] && return 0
|
|
|
|
local -a uniq=()
|
|
local v seen u
|
|
for v in "${vals[@]}"; do
|
|
seen=0
|
|
for u in "${uniq[@]:-}"; do [[ "$u" == "$v" ]] && { seen=1; break; }; done
|
|
[[ "$seen" -eq 0 ]] && uniq+=("$v")
|
|
done
|
|
|
|
local choice=""
|
|
if [[ ${#uniq[@]} -eq 1 ]]; then
|
|
choice="${uniq[0]}"
|
|
ssr::log "Consolidating ${#vals[@]} SSR_CLOUD_PROVIDER line(s) → $choice"
|
|
elif [[ -t 0 ]]; then
|
|
printf '\n' >&2
|
|
ssr::warn "Config $cfg defines multiple different SSR_CLOUD_PROVIDER values."
|
|
local i=1
|
|
for v in "${uniq[@]}"; do
|
|
printf ' %d) %s\n' "$i" "$v" >&2
|
|
i=$((i + 1))
|
|
done
|
|
local pick
|
|
while true; do
|
|
read -r -p "Select provider to keep [1-${#uniq[@]}]: " pick || pick=""
|
|
[[ "$pick" =~ ^[0-9]+$ ]] || { ssr::warn "Enter a number between 1 and ${#uniq[@]}."; continue; }
|
|
(( pick >= 1 && pick <= ${#uniq[@]} )) || { ssr::warn "Out of range."; continue; }
|
|
choice="${uniq[$((pick - 1))]}"
|
|
break
|
|
done
|
|
ssr::ok "SSR_CLOUD_PROVIDER=$choice will be written to $cfg"
|
|
else
|
|
ssr::die "Config $cfg has conflicting SSR_CLOUD_PROVIDER lines (${uniq[*]}). Keep one value or run ssr from a terminal to choose."
|
|
fi
|
|
|
|
local tmp newtmp bak
|
|
tmp="$(mktemp "${TMPDIR:-/tmp}/ssr-cfg.XXXXXX")"
|
|
newtmp="$(mktemp "${TMPDIR:-/tmp}/ssr-cfg.XXXXXX")"
|
|
bak="${cfg}.bak.$(date +%s)"
|
|
cp -p "$cfg" "$bak" 2>/dev/null || cp "$cfg" "$bak"
|
|
grep -v '^SSR_CLOUD_PROVIDER=' "$cfg" > "$tmp"
|
|
local ins=0
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
if [[ "$ins" -eq 0 && "$line" == '# Provider:'* ]]; then
|
|
printf '%s\n' "$line"
|
|
printf 'SSR_CLOUD_PROVIDER=%s\n' "$choice"
|
|
ins=1
|
|
continue
|
|
fi
|
|
printf '%s\n' "$line"
|
|
done < "$tmp" > "$newtmp"
|
|
[[ "$ins" -eq 1 ]] || printf 'SSR_CLOUD_PROVIDER=%s\n' "$choice" >> "$newtmp"
|
|
mv "$newtmp" "$cfg"
|
|
rm -f "$tmp"
|
|
ssr::log "Config updated (previous copy: $bak)"
|
|
}
|
|
|
|
# Source config file if it exists. Validates that only known KEY=VALUE lines
|
|
# are present to avoid arbitrary code execution from a tampered config.
|
|
ssr::load_config() {
|
|
local cfg="${1:-${SSR_CONFIG:-$HOME/.config/ssr/ssr.conf}}"
|
|
export SSR_CONFIG="$cfg"
|
|
[[ -f "$cfg" ]] || return 0
|
|
ssr::config::resolve_cloud_provider_duplicates "$cfg"
|
|
if grep -Eq -v '^[[:space:]]*(#|$|[A-Z_][A-Z0-9_]*=)' "$cfg"; then
|
|
ssr::die "Config $cfg contains invalid lines (only KEY=VALUE allowed)"
|
|
fi
|
|
# shellcheck disable=SC1090
|
|
source "$cfg"
|
|
}
|