#!/bin/bash # Common utility functions and variables for torrent-mover # Global Runtime Variables DRY_RUN=0 INTERACTIVE=0 CACHE_WARMUP=0 DEBUG=0 # To avoid reprocessing the same source directory (across different torrents) declare -A processed_source_dirs declare -A CHECKED_MOUNTS=() declare -A PATH_CACHE # Logging Functions # All log messages go to stderr. log_debug() { if [[ "${DEBUG}" -eq 1 ]]; then echo -e "[DEBUG] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 [[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[DEBUG] $*" fi } log_info() { echo -e "[INFO] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 [[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[INFO] $*" } log_warn() { echo -e "[WARN] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 [[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[WARN] $*" } log_error() { echo -e "[ERROR] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 [[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[ERROR] $*" } # Error Handling & Notifications error_handler() { local lineno="$1" local msg="$2" log_error "Error on line ${lineno}: ${msg}" # Optionally send a notification (e.g., email) return 1 } # translate_source: Converts the Transmission‑reported path into the local path. translate_source() { local src="$1" echo "${src/#${TRANSMISSION_PATH_PREFIX}/${LOCAL_PATH_PREFIX}}" } # parse_args: Processes command‑line options. parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=1; shift ;; --interactive) INTERACTIVE=1; shift ;; --cache-warmup) CACHE_WARMUP=1; shift ;; --debug) DEBUG=1; shift ;; --help) echo "Usage: $0 [--dry-run] [--interactive] [--cache-warmup] [--debug]" >&2 exit 0 ;; *) echo "Invalid option: $1" >&2; exit 1 ;; esac done } # check_dependencies: Ensures required commands are available. check_dependencies() { local deps=("transmission-remote" "unzip" "7z" "parallel" "bc") for dep in "${deps[@]}"; do command -v "${dep}" >/dev/null 2>&1 || { log_error "Missing dependency: ${dep}"; exit 1; } done # Check for unrar or unrar-free if command -v unrar &>/dev/null; then log_debug "Found unrar command" elif command -v unrar-free &>/dev/null; then log_debug "Found unrar-free command" # Create an alias for unrar to point to unrar-free alias unrar="unrar-free" else log_error "Missing dependency: unrar or unrar-free" exit 1 fi } # check_disk_usage: Warn if disk usage is over 90%. check_disk_usage() { local dir="$1" [[ -z "${dir}" ]] && return if ! df -P "${dir}" &>/dev/null; then log_warn "Directory not found: ${dir}" return fi local mount_point mount_point=$(df -P "${dir}" | awk 'NR==2 {print $6}') [[ -z "${mount_point}" ]] && return if [[ -z "${CHECKED_MOUNTS["${mount_point}"]+x}" ]]; then local usage usage=$(df -P "${dir}" | awk 'NR==2 {sub(/%/, "", $5); print $5}') if (( usage >= 90 )); then log_warn "Storage warning: ${mount_point} at ${usage}% capacity" fi CHECKED_MOUNTS["${mount_point}"]=1 fi } # run_command_safely: Safer version of command execution that prevents injection run_command_safely() { # Instead of using eval with a command string, this function accepts the command and arguments separately # This prevents command injection vulnerabilities if [[ $# -eq 0 ]]; then log_error "No command provided to run_command_safely" return 1 fi log_debug "Running command: $*" "$@" return $? } # retry_command: Execute a command with retries retry_command() { local cmd="$1" local max_attempts="${2:-3}" # Default to 3 attempts local wait_time="${3:-10}" # Default to 10 seconds wait between attempts local attempt=1 local exit_code=0 local error_output="" # Create a temporary file for capturing error output local error_file error_file=$(mktemp) while (( attempt <= max_attempts )); do log_debug "Attempt $attempt of $max_attempts: $cmd" # Execute command and capture both exit code and stderr error_output=$( { eval "$cmd"; exit_code=$?; } 2>&1 > >(tee /dev/stderr) ) if [[ ${exit_code} -eq 0 ]]; then log_debug "Command succeeded on attempt $attempt" rm -f "${error_file}" return 0 else # Log detailed error information echo "${error_output}" > "${error_file}" log_warn "Command failed (attempt $attempt, exit code: ${exit_code})" log_debug "Error details: $(head -n 5 "${error_file}")" if (( attempt == max_attempts )); then log_error "Maximum attempts reached for command, last exit code: ${exit_code}" log_error "Last error output: $(head -n 10 "${error_file}")" rm -f "${error_file}" return ${exit_code} fi # Exponential backoff - wait longer for each successive attempt local adjusted_wait=$((wait_time * attempt)) log_debug "Waiting ${adjusted_wait} seconds before retry" sleep ${adjusted_wait} (( attempt++ )) fi done rm -f "${error_file}" return 1 } # run_in_transaction: Runs commands with an atomic operation guarantee # If any command fails, attempts to roll back changes run_in_transaction() { local action_desc="$1" local cleanup_cmd="$2" local main_cmd="$3" log_debug "Starting transaction: ${action_desc}" # Create marker file to indicate transaction in progress local transaction_id transaction_id=$(date +%s)-$$ local transaction_marker="/tmp/torrent-mover-transaction-${transaction_id}" echo "${action_desc}" > "${transaction_marker}" # Execute the main command if ! eval "${main_cmd}"; then log_error "Transaction failed: ${action_desc}" # Only run cleanup if it exists if [[ -n "${cleanup_cmd}" ]]; then log_info "Attempting transaction rollback" if ! eval "${cleanup_cmd}"; then log_error "Rollback failed, manual intervention may be required" else log_info "Rollback completed successfully" fi fi # Clean up marker rm -f "${transaction_marker}" return 1 fi # Clean up marker on success rm -f "${transaction_marker}" log_debug "Transaction completed successfully: ${action_desc}" return 0 } # validate_directories: Ensure required directories exist and are writable validate_directories() { local directories=("$@") for dir in "${directories[@]}"; do if [[ ! -d "${dir}" ]]; then log_error "Directory missing: ${dir}" return 1 fi if [[ ! -w "${dir}" ]]; then log_error "Write permission denied: ${dir}" return 1 fi done return 0 }