chore: initial commit of docker-tools
This commit is contained in:
712
bash/lib/common.sh
Executable file
712
bash/lib/common.sh
Executable file
@@ -0,0 +1,712 @@
|
||||
#!/bin/bash
|
||||
# DocWell Shared Library
|
||||
# Common functions used across all docker-tools bash scripts
|
||||
# Source this file: source "$(dirname "$0")/lib/common.sh"
|
||||
|
||||
# Version
|
||||
LIB_VERSION="2.6.2"
|
||||
|
||||
# ─── Colors (with NO_COLOR support) ──────────────────────────────────────────
|
||||
if [[ -n "${NO_COLOR:-}" ]] || [[ ! -t 1 ]]; then
|
||||
RED='' GREEN='' YELLOW='' BLUE='' GRAY='' NC='' BOLD='' CYAN=''
|
||||
else
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
GRAY='\033[0;90m'
|
||||
NC='\033[0m'
|
||||
BOLD='\033[1m'
|
||||
CYAN='\033[0;36m'
|
||||
fi
|
||||
|
||||
# ─── Common defaults ─────────────────────────────────────────────────────────
|
||||
STACKS_DIR="${STACKS_DIR:-/opt/stacks}"
|
||||
QUIET="${QUIET:-false}"
|
||||
YES="${YES:-false}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
AUTO_INSTALL="${AUTO_INSTALL:-false}"
|
||||
LOG_FILE="${LOG_FILE:-/tmp/docwell/docwell.log}"
|
||||
DEBUG="${DEBUG:-false}"
|
||||
VERBOSE="${VERBOSE:-false}"
|
||||
|
||||
# Concurrency limit (bounded by nproc)
|
||||
MAX_PARALLEL="${MAX_PARALLEL:-$(nproc 2>/dev/null || echo 4)}"
|
||||
|
||||
# ─── Debug and Tracing ──────────────────────────────────────────────────────
|
||||
# Enable xtrace if DEBUG is set (but preserve existing set options)
|
||||
# Note: This should be called after scripts set their own 'set' options
|
||||
enable_debug_trace() {
|
||||
if [[ "${DEBUG}" == "true" ]]; then
|
||||
# Check if we're already in a script context with set -euo pipefail
|
||||
# If so, preserve those flags when enabling -x
|
||||
if [[ "${-}" == *e* ]] && [[ "${-}" == *u* ]] && [[ "${-}" == *o* ]]; then
|
||||
set -euxo pipefail
|
||||
else
|
||||
set -x
|
||||
fi
|
||||
export PS4='+ [${BASH_SOURCE##*/}:${LINENO}] ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
|
||||
log_debug "Debug trace enabled (xtrace)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function call tracing (when DEBUG or VERBOSE is enabled)
|
||||
debug_trace() {
|
||||
[[ "$DEBUG" == "true" || "$VERBOSE" == "true" ]] || return 0
|
||||
local func="${FUNCNAME[1]:-main}"
|
||||
local line="${BASH_LINENO[0]:-?}"
|
||||
local file="${BASH_SOURCE[1]##*/}"
|
||||
echo -e "${GRAY}[DEBUG]${NC} ${file}:${line} ${func}()" >&2
|
||||
}
|
||||
|
||||
# Debug logging
|
||||
log_debug() {
|
||||
[[ "$DEBUG" == "true" || "$VERBOSE" == "true" ]] || return 0
|
||||
local msg="$*"
|
||||
local timestamp
|
||||
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
write_log "$timestamp [DEBUG] $msg"
|
||||
echo -e "${GRAY}[DEBUG]${NC} $msg" >&2
|
||||
}
|
||||
|
||||
# Verbose logging
|
||||
log_verbose() {
|
||||
[[ "$VERBOSE" == "true" ]] || return 0
|
||||
local msg="$*"
|
||||
local timestamp
|
||||
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
write_log "$timestamp [VERBOSE] $msg"
|
||||
[[ "$QUIET" == "false" ]] && echo -e "${GRAY}[VERBOSE]${NC} $msg"
|
||||
}
|
||||
|
||||
# ─── Spinner ──────────────────────────────────────────────────────────────────
|
||||
_SPINNER_PID=""
|
||||
_SPINNER_FRAMES=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
|
||||
|
||||
show_spinner() {
|
||||
local msg="${1:-Working...}"
|
||||
[[ "$QUIET" == "true" ]] && return
|
||||
[[ "$DEBUG" == "true" ]] && return # Don't show spinner in debug mode
|
||||
|
||||
(
|
||||
local i=0
|
||||
while true; do
|
||||
printf "\r%b%s%b %s" "$CYAN" "${_SPINNER_FRAMES[$((i % ${#_SPINNER_FRAMES[@]}))]}" "$NC" "$msg"
|
||||
((i++))
|
||||
sleep 0.08
|
||||
done
|
||||
) &
|
||||
_SPINNER_PID=$!
|
||||
disown "$_SPINNER_PID" 2>/dev/null || true
|
||||
log_debug "Spinner started (PID: $_SPINNER_PID)"
|
||||
}
|
||||
|
||||
hide_spinner() {
|
||||
if [[ -n "$_SPINNER_PID" ]]; then
|
||||
log_debug "Stopping spinner (PID: $_SPINNER_PID)"
|
||||
kill "$_SPINNER_PID" 2>/dev/null || true
|
||||
wait "$_SPINNER_PID" 2>/dev/null || true
|
||||
_SPINNER_PID=""
|
||||
printf "\r\033[K" # Clear the spinner line
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Logging ──────────────────────────────────────────────────────────────────
|
||||
write_log() {
|
||||
local msg="$1"
|
||||
local log_dir
|
||||
log_dir=$(dirname "$LOG_FILE")
|
||||
|
||||
# Try to create log directory if it doesn't exist
|
||||
if [[ ! -d "$log_dir" ]]; then
|
||||
mkdir -p "$log_dir" 2>/dev/null || {
|
||||
# If we can't create the directory, try /tmp as fallback
|
||||
if [[ "$log_dir" != "/tmp"* ]]; then
|
||||
log_dir="/tmp/docwell"
|
||||
mkdir -p "$log_dir" 2>/dev/null || return 0
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
fi
|
||||
|
||||
# Check if we can write to the log file
|
||||
if [[ -w "$log_dir" ]] 2>/dev/null || [[ -w "$(dirname "$log_dir")" ]] 2>/dev/null; then
|
||||
echo "$msg" >> "$LOG_FILE" 2>/dev/null || {
|
||||
# Fallback to /tmp if original location fails
|
||||
if [[ "$LOG_FILE" != "/tmp/"* ]]; then
|
||||
echo "$msg" >> "/tmp/docwell/fallback.log" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
log_info() {
|
||||
local msg="$*"
|
||||
local timestamp
|
||||
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
write_log "$timestamp [INFO] $msg"
|
||||
[[ "$QUIET" == "false" ]] && echo -e "${GREEN}[INFO]${NC} $msg"
|
||||
debug_trace
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
local msg="$*"
|
||||
local timestamp
|
||||
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
write_log "$timestamp [WARN] $msg"
|
||||
[[ "$QUIET" == "false" ]] && echo -e "${YELLOW}[WARN]${NC} $msg" >&2
|
||||
}
|
||||
|
||||
log_error() {
|
||||
local msg="$*"
|
||||
local timestamp
|
||||
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
write_log "$timestamp [ERROR] $msg"
|
||||
echo -e "${RED}[ERROR]${NC} $msg" >&2
|
||||
debug_trace
|
||||
# In debug mode, show stack trace
|
||||
if [[ "$DEBUG" == "true" ]]; then
|
||||
local i=0
|
||||
while caller $i >/dev/null 2>&1; do
|
||||
local frame
|
||||
frame=$(caller $i)
|
||||
echo -e "${GRAY} ->${NC} $frame" >&2
|
||||
((i++))
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
log_success() {
|
||||
local msg="$*"
|
||||
local timestamp
|
||||
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
write_log "$timestamp [OK] $msg"
|
||||
[[ "$QUIET" == "false" ]] && echo -e "${GREEN}[✓]${NC} $msg"
|
||||
}
|
||||
|
||||
log_header() {
|
||||
local msg="$1"
|
||||
write_log "=== $msg ==="
|
||||
echo -e "${BLUE}${BOLD}═══════════════════════════════════════════════${NC}"
|
||||
echo -e "${BLUE}${BOLD} $msg${NC}"
|
||||
echo -e "${BLUE}${BOLD}═══════════════════════════════════════════════${NC}"
|
||||
}
|
||||
|
||||
log_separator() {
|
||||
echo -e "${GRAY}───────────────────────────────────────────────${NC}"
|
||||
}
|
||||
|
||||
# ─── Prompts ──────────────────────────────────────────────────────────────────
|
||||
confirm() {
|
||||
local prompt="$1"
|
||||
if [[ "$YES" == "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
read -p "$(echo -e "${YELLOW}?${NC} $prompt (y/N): ")" -n 1 -r
|
||||
echo
|
||||
[[ $REPLY =~ ^[Yy]$ ]]
|
||||
}
|
||||
|
||||
# ─── Argument validation ──────────────────────────────────────────────────────
|
||||
# Call from parse_args for options that require a value. Returns 1 if missing/invalid.
|
||||
require_arg() {
|
||||
local opt="${1:-}"
|
||||
local val="${2:-}"
|
||||
if [[ -z "$val" ]] || [[ "$val" == -* ]]; then
|
||||
log_error "Option $opt requires a value"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ─── Validation ───────────────────────────────────────────────────────────────
|
||||
validate_stack_name() {
|
||||
local name="$1"
|
||||
debug_trace
|
||||
log_debug "Validating stack name: '$name'"
|
||||
|
||||
if [[ -z "$name" ]] || [[ "$name" == "." ]] || [[ "$name" == ".." ]]; then
|
||||
log_error "Invalid stack name: cannot be empty, '.', or '..'"
|
||||
return 1
|
||||
fi
|
||||
if [[ "$name" =~ [^a-zA-Z0-9._-] ]]; then
|
||||
log_error "Invalid stack name: contains invalid characters (allowed: a-z, A-Z, 0-9, ., _, -)"
|
||||
return 1
|
||||
fi
|
||||
if [[ ${#name} -gt 64 ]]; then
|
||||
log_error "Invalid stack name: too long (max 64 characters, got ${#name})"
|
||||
return 1
|
||||
fi
|
||||
log_debug "Stack name validation passed"
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_port() {
|
||||
local port="$1"
|
||||
debug_trace
|
||||
log_debug "Validating port: '$port'"
|
||||
|
||||
if ! [[ "$port" =~ ^[0-9]+$ ]] || [[ "$port" -lt 1 ]] || [[ "$port" -gt 65535 ]]; then
|
||||
log_error "Invalid port: $port (must be 1-65535)"
|
||||
return 1
|
||||
fi
|
||||
log_debug "Port validation passed"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ─── Config file support ─────────────────────────────────────────────────────
|
||||
DOCWELL_CONFIG_FILE="${HOME}/.config/docwell/config"
|
||||
|
||||
load_config() {
|
||||
[[ -f "$DOCWELL_CONFIG_FILE" ]] || return 0
|
||||
|
||||
while IFS= read -r line; do
|
||||
# Skip comments and empty lines
|
||||
line=$(echo "$line" | xargs)
|
||||
[[ -z "$line" || "$line" == \#* ]] && continue
|
||||
|
||||
# Split on first '=' so values can contain '='
|
||||
key="${line%%=*}"
|
||||
key=$(echo "$key" | xargs)
|
||||
value="${line#*=}"
|
||||
value=$(echo "$value" | xargs | sed 's/^"//;s/"$//')
|
||||
|
||||
[[ -z "$key" ]] && continue
|
||||
|
||||
case "$key" in
|
||||
StacksDir) STACKS_DIR="$value" ;;
|
||||
BackupBase) BACKUP_BASE="$value" ;;
|
||||
LogFile) LOG_FILE="$value" ;;
|
||||
OldHost) OLD_HOST="$value" ;;
|
||||
OldPort) OLD_PORT="$value" ;;
|
||||
OldUser) OLD_USER="$value" ;;
|
||||
NewHost) NEW_HOST="$value" ;;
|
||||
NewPort) NEW_PORT="$value" ;;
|
||||
NewUser) NEW_USER="$value" ;;
|
||||
BandwidthMB) BANDWIDTH_MB="$value" ;;
|
||||
TransferRetries) TRANSFER_RETRIES="$value" ;;
|
||||
esac
|
||||
done < "$DOCWELL_CONFIG_FILE"
|
||||
}
|
||||
|
||||
# ─── Compose file detection ──────────────────────────────────────────────────
|
||||
get_compose_file() {
|
||||
local stack_path="$1"
|
||||
debug_trace
|
||||
log_debug "Looking for compose file in: $stack_path"
|
||||
|
||||
if [[ ! -d "$stack_path" ]]; then
|
||||
log_debug "Stack path does not exist: $stack_path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
for f in compose.yaml compose.yml docker-compose.yaml docker-compose.yml; do
|
||||
if [[ -f "$stack_path/$f" ]]; then
|
||||
log_debug "Found compose file: $f"
|
||||
echo "$f"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
log_debug "No compose file found in: $stack_path"
|
||||
return 1
|
||||
}
|
||||
|
||||
has_compose_file() {
|
||||
local stack_path="$1"
|
||||
get_compose_file "$stack_path" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# ─── Stack operations ────────────────────────────────────────────────────────
|
||||
get_stacks() {
|
||||
debug_trace
|
||||
log_debug "Discovering stacks in: $STACKS_DIR"
|
||||
|
||||
if [[ ! -d "$STACKS_DIR" ]]; then
|
||||
log_debug "Stacks directory does not exist: $STACKS_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local stacks=()
|
||||
local dir_count=0
|
||||
|
||||
while IFS= read -r -d '' dir; do
|
||||
((dir_count++))
|
||||
local name
|
||||
name=$(basename "$dir")
|
||||
log_debug "Found directory: $name"
|
||||
if validate_stack_name "$name" >/dev/null 2>&1; then
|
||||
stacks+=("$name")
|
||||
log_debug "Added valid stack: $name"
|
||||
else
|
||||
log_debug "Skipped invalid stack name: $name"
|
||||
fi
|
||||
done < <(find "$STACKS_DIR" -maxdepth 1 -mindepth 1 -type d -print0 2>/dev/null | sort -z)
|
||||
|
||||
log_debug "Found $dir_count directories, ${#stacks[@]} valid stacks"
|
||||
|
||||
# Fallback: also discover stacks from docker ps
|
||||
log_debug "Checking running containers for additional stacks..."
|
||||
local docker_stacks
|
||||
docker_stacks=$(docker ps --format '{{.Labels}}' 2>/dev/null | \
|
||||
grep -oP 'com\.docker\.compose\.project=\K[^,]+' | sort -u || true)
|
||||
|
||||
local added_from_docker=0
|
||||
for ds in $docker_stacks; do
|
||||
[[ -z "$ds" ]] && continue
|
||||
local found=false
|
||||
for s in "${stacks[@]}"; do
|
||||
[[ "$s" == "$ds" ]] && { found=true; break; }
|
||||
done
|
||||
if [[ "$found" == "false" ]] && validate_stack_name "$ds" >/dev/null 2>&1; then
|
||||
stacks+=("$ds")
|
||||
((added_from_docker++))
|
||||
log_debug "Added stack from docker ps: $ds"
|
||||
fi
|
||||
done
|
||||
|
||||
log_debug "Total stacks discovered: ${#stacks[@]} (${added_from_docker} from docker ps)"
|
||||
printf '%s\n' "${stacks[@]}"
|
||||
}
|
||||
|
||||
is_stack_running() {
|
||||
local stack="$1"
|
||||
local stack_path="${2:-$STACKS_DIR/$stack}"
|
||||
local compose_file
|
||||
|
||||
debug_trace
|
||||
log_debug "Checking if stack '$stack' is running (path: $stack_path)"
|
||||
|
||||
compose_file=$(get_compose_file "$stack_path") || {
|
||||
log_debug "No compose file found for stack: $stack"
|
||||
return 1
|
||||
}
|
||||
|
||||
local running
|
||||
running=$(docker compose -f "$stack_path/$compose_file" ps -q 2>/dev/null | grep -c . || echo "0")
|
||||
|
||||
if [[ "$running" -gt 0 ]]; then
|
||||
log_debug "Stack '$stack' is running ($running container(s))"
|
||||
return 0
|
||||
else
|
||||
log_debug "Stack '$stack' is not running"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
get_stack_size() {
|
||||
local stack="$1"
|
||||
local stack_path="$STACKS_DIR/$stack"
|
||||
debug_trace
|
||||
|
||||
if [[ ! -d "$stack_path" ]]; then
|
||||
log_debug "Stack path does not exist: $stack_path"
|
||||
echo "?"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local size
|
||||
size=$(du -sh "$stack_path" 2>/dev/null | awk '{print $1}' || echo "?")
|
||||
log_debug "Stack size for $stack: $size"
|
||||
echo "$size"
|
||||
}
|
||||
|
||||
get_service_volumes() {
|
||||
local service="$1"
|
||||
local stack_path="${2:-$STACKS_DIR/$service}"
|
||||
local compose_file
|
||||
debug_trace
|
||||
log_debug "Getting volumes for service: $service (path: $stack_path)"
|
||||
|
||||
compose_file=$(get_compose_file "$stack_path") || {
|
||||
log_debug "No compose file found, cannot get volumes"
|
||||
return 1
|
||||
}
|
||||
|
||||
local volumes
|
||||
volumes=$(docker compose -f "$stack_path/$compose_file" config --volumes 2>/dev/null || true)
|
||||
|
||||
if [[ -n "$volumes" ]]; then
|
||||
local vol_count
|
||||
vol_count=$(echo "$volumes" | grep -c . || echo "0")
|
||||
log_debug "Found $vol_count volume(s) for service: $service"
|
||||
else
|
||||
log_debug "No volumes found for service: $service"
|
||||
fi
|
||||
|
||||
echo "$volumes"
|
||||
}
|
||||
|
||||
# ─── Dependency management ───────────────────────────────────────────────────
|
||||
install_deps_debian() {
|
||||
local deps=("$@")
|
||||
local apt_pkgs=()
|
||||
for dep in "${deps[@]}"; do
|
||||
case "$dep" in
|
||||
docker) apt_pkgs+=("docker.io") ;;
|
||||
zstd) apt_pkgs+=("zstd") ;;
|
||||
rsync) apt_pkgs+=("rsync") ;;
|
||||
rclone) apt_pkgs+=("rclone") ;;
|
||||
*) apt_pkgs+=("$dep") ;;
|
||||
esac
|
||||
done
|
||||
if [[ ${#apt_pkgs[@]} -gt 0 ]]; then
|
||||
sudo apt update && sudo apt install -y "${apt_pkgs[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
install_deps_arch() {
|
||||
local deps=("$@")
|
||||
local pacman_pkgs=()
|
||||
for dep in "${deps[@]}"; do
|
||||
case "$dep" in
|
||||
docker) pacman_pkgs+=("docker") ;;
|
||||
zstd) pacman_pkgs+=("zstd") ;;
|
||||
rsync) pacman_pkgs+=("rsync") ;;
|
||||
rclone) pacman_pkgs+=("rclone") ;;
|
||||
*) pacman_pkgs+=("$dep") ;;
|
||||
esac
|
||||
done
|
||||
if [[ ${#pacman_pkgs[@]} -gt 0 ]]; then
|
||||
sudo pacman -Sy --noconfirm "${pacman_pkgs[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
local deps=("$@")
|
||||
if command -v apt &>/dev/null; then
|
||||
install_deps_debian "${deps[@]}"
|
||||
elif command -v pacman &>/dev/null; then
|
||||
install_deps_arch "${deps[@]}"
|
||||
else
|
||||
log_error "No supported package manager found (apt/pacman)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_docker() {
|
||||
debug_trace
|
||||
log_debug "Checking Docker installation and daemon status"
|
||||
|
||||
if ! command -v docker &>/dev/null; then
|
||||
log_error "Docker not found. Please install Docker first."
|
||||
echo -e "${BLUE}Install commands:${NC}"
|
||||
command -v apt &>/dev/null && echo -e " ${GREEN}Debian/Ubuntu:${NC} sudo apt install docker.io"
|
||||
command -v pacman &>/dev/null && echo -e " ${GREEN}Arch Linux:${NC} sudo pacman -S docker"
|
||||
|
||||
if [[ "$AUTO_INSTALL" == "true" ]]; then
|
||||
log_info "Auto-installing dependencies..."
|
||||
install_dependencies "docker"
|
||||
return $?
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_debug "Docker command found: $(command -v docker)"
|
||||
log_debug "Docker version: $(docker --version 2>&1 || echo 'unknown')"
|
||||
|
||||
if ! docker info &>/dev/null; then
|
||||
log_error "Docker daemon not running or not accessible."
|
||||
log_debug "Attempting to get more details..."
|
||||
docker info 2>&1 | head -5 | while IFS= read -r line; do
|
||||
log_debug " $line"
|
||||
done || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_debug "Docker daemon is accessible"
|
||||
return 0
|
||||
}
|
||||
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
if ! sudo -n true 2>/dev/null; then
|
||||
log_error "Root/sudo privileges required"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ─── Screen control ───────────────────────────────────────────────────────────
|
||||
clear_screen() {
|
||||
printf '\033[H\033[2J'
|
||||
}
|
||||
|
||||
# ─── Elapsed time formatting ─────────────────────────────────────────────────
|
||||
format_elapsed() {
|
||||
local seconds="$1"
|
||||
# Handle invalid input
|
||||
if ! [[ "$seconds" =~ ^[0-9]+$ ]]; then
|
||||
echo "0s"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$seconds" -lt 60 ]]; then
|
||||
echo "${seconds}s"
|
||||
elif [[ "$seconds" -lt 3600 ]]; then
|
||||
local mins=$((seconds / 60))
|
||||
local secs=$((seconds % 60))
|
||||
echo "${mins}m ${secs}s"
|
||||
else
|
||||
local hours=$((seconds / 3600))
|
||||
local mins=$((seconds % 3600 / 60))
|
||||
local secs=$((seconds % 60))
|
||||
echo "${hours}h ${mins}m ${secs}s"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Compression helper ──────────────────────────────────────────────────────
|
||||
get_compressor() {
|
||||
local method="${1:-zstd}"
|
||||
case "$method" in
|
||||
gzip)
|
||||
echo "gzip:.tar.gz"
|
||||
;;
|
||||
zstd|*)
|
||||
if command -v zstd &>/dev/null; then
|
||||
echo "zstd -T0:.tar.zst"
|
||||
else
|
||||
log_warn "zstd not found, falling back to gzip"
|
||||
echo "gzip:.tar.gz"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ─── Cleanup trap helper ─────────────────────────────────────────────────────
|
||||
# Usage: register_cleanup "command to run"
|
||||
_CLEANUP_CMDS=()
|
||||
|
||||
register_cleanup() {
|
||||
local cmd="$1"
|
||||
debug_trace
|
||||
log_debug "Registering cleanup command: $cmd"
|
||||
_CLEANUP_CMDS+=("$cmd")
|
||||
}
|
||||
|
||||
_run_cleanups() {
|
||||
hide_spinner
|
||||
if [[ ${#_CLEANUP_CMDS[@]} -gt 0 ]]; then
|
||||
log_debug "Running ${#_CLEANUP_CMDS[@]} cleanup command(s)"
|
||||
for cmd in "${_CLEANUP_CMDS[@]}"; do
|
||||
log_debug "Executing cleanup: $cmd"
|
||||
eval "$cmd" 2>/dev/null || {
|
||||
log_warn "Cleanup command failed: $cmd"
|
||||
}
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
trap _run_cleanups EXIT INT TERM
|
||||
|
||||
# ─── Dry-run wrapper ─────────────────────────────────────────────────────────
|
||||
# Usage: run_or_dry "description" command arg1 arg2 ...
|
||||
run_or_dry() {
|
||||
local desc="$1"
|
||||
shift
|
||||
debug_trace
|
||||
log_debug "run_or_dry: $desc"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "[DRY-RUN] Would: $desc"
|
||||
log_verbose "[DRY-RUN] Command: $*"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_verbose "Executing: $*"
|
||||
"$@"
|
||||
local rc=$?
|
||||
log_debug "Command exit code: $rc"
|
||||
return $rc
|
||||
}
|
||||
|
||||
# ─── Parallel execution with bounded concurrency ─────────────────────────────
|
||||
# Usage: parallel_run max_jobs command arg1 arg2 ...
|
||||
# Each arg is processed by spawning: command arg &
|
||||
_parallel_pids=()
|
||||
|
||||
parallel_wait() {
|
||||
debug_trace
|
||||
if [[ ${#_parallel_pids[@]} -gt 0 ]]; then
|
||||
log_debug "Waiting for ${#_parallel_pids[@]} parallel jobs to complete"
|
||||
local failed_count=0
|
||||
for pid in "${_parallel_pids[@]}"; do
|
||||
local exit_code=0
|
||||
if ! wait "$pid" 2>/dev/null; then
|
||||
exit_code=$?
|
||||
log_debug "Job $pid failed with exit code: $exit_code"
|
||||
((failed_count++)) || true
|
||||
else
|
||||
log_debug "Job $pid completed successfully"
|
||||
fi
|
||||
done
|
||||
if [[ $failed_count -gt 0 ]]; then
|
||||
log_warn "$failed_count parallel job(s) failed"
|
||||
fi
|
||||
fi
|
||||
_parallel_pids=()
|
||||
}
|
||||
|
||||
parallel_throttle() {
|
||||
local max_jobs="${1:-$MAX_PARALLEL}"
|
||||
debug_trace
|
||||
log_debug "Throttling parallel jobs: ${#_parallel_pids[@]}/$max_jobs"
|
||||
|
||||
while [[ ${#_parallel_pids[@]} -ge "$max_jobs" ]]; do
|
||||
local new_pids=()
|
||||
for pid in "${_parallel_pids[@]}"; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
new_pids+=("$pid")
|
||||
else
|
||||
log_debug "Job $pid completed, removing from throttle list"
|
||||
fi
|
||||
done
|
||||
_parallel_pids=("${new_pids[@]+"${new_pids[@]}"}")
|
||||
[[ ${#_parallel_pids[@]} -ge "$max_jobs" ]] && sleep 0.1
|
||||
done
|
||||
log_debug "Throttle check passed: ${#_parallel_pids[@]}/$max_jobs"
|
||||
}
|
||||
|
||||
# ─── SSH helpers ──────────────────────────────────────────────────────────────
|
||||
ssh_cmd() {
|
||||
local host="$1" port="$2" user="$3"
|
||||
shift 3
|
||||
|
||||
debug_trace
|
||||
log_debug "Executing SSH command: host=$host, port=$port, user=$user"
|
||||
log_verbose "Command: $*"
|
||||
|
||||
if [[ "$host" == "local" ]]; then
|
||||
log_debug "Local execution (no SSH)"
|
||||
"$@"
|
||||
local rc=$?
|
||||
log_debug "Local command exit code: $rc"
|
||||
return $rc
|
||||
fi
|
||||
|
||||
validate_port "$port" || return 1
|
||||
|
||||
log_debug "SSH connection: $user@$host:$port"
|
||||
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -p "$port" "$user@$host" "$@"
|
||||
local rc=$?
|
||||
log_debug "SSH command exit code: $rc"
|
||||
return $rc
|
||||
}
|
||||
|
||||
test_ssh() {
|
||||
local host="$1" port="$2" user="$3"
|
||||
|
||||
[[ "$host" == "local" ]] && return 0
|
||||
|
||||
validate_port "$port" || return 1
|
||||
|
||||
if ssh_cmd "$host" "$port" "$user" 'echo OK' >/dev/null 2>&1; then
|
||||
log_success "SSH connection to $user@$host:$port successful"
|
||||
return 0
|
||||
else
|
||||
log_error "SSH connection failed to $user@$host:$port"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
Reference in New Issue
Block a user