#!/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 if [[ "${USE_SYSLOG}" == "true" ]]; then logger -t torrent-mover "[DEBUG] $*" || true fi fi } log_info() { echo -e "[INFO] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 if [[ "${USE_SYSLOG}" == "true" ]]; then logger -t torrent-mover "[INFO] $*" || true fi } log_warn() { echo -e "[WARN] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 if [[ "${USE_SYSLOG}" == "true" ]]; then logger -t torrent-mover "[WARN] $*" || true fi } log_error() { echo -e "[ERROR] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 if [[ "${USE_SYSLOG}" == "true" ]]; then logger -t torrent-mover "[ERROR] $*" || true fi } # 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 log_debug "Checking disk usage for directory: ${dir}" 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}') if [[ -z "${mount_point}" ]]; then log_warn "Could not determine mount point for: ${dir}" return fi log_debug "Mount point for ${dir} is ${mount_point}" # Initialize CHECKED_MOUNTS as an empty array if not already done if [[ -z "${CHECKED_MOUNTS+x}" ]]; then declare -A CHECKED_MOUNTS fi # Check if we've already checked this mount point if [[ -z "${CHECKED_MOUNTS[${mount_point}]+x}" ]]; then local usage usage=$(df -P "${dir}" | awk 'NR==2 {sub(/%/, "", $5); print $5}') log_debug "Usage for ${mount_point}: ${usage}%" if (( usage >= 90 )); then log_warn "Storage warning: ${mount_point} at ${usage}% capacity" fi CHECKED_MOUNTS[${mount_point}]=1 else log_debug "Mount point ${mount_point} already checked" 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 command_output="" # Create a temporary file for capturing output local output_file output_file=$(mktemp) # Use a more verbose logging for this command - always log, not just in debug mode log_info "Executing command: $cmd" while (( attempt <= max_attempts )); do log_info "Attempt $attempt of $max_attempts: $cmd" # Execute command directly and capture output and exit code command_output=$(eval "$cmd" 2>&1) exit_code=$? echo "$command_output" > "${output_file}" # Always log the first 10 lines of output log_info "Command output (first 10 lines):" head -n 10 "${output_file}" | while IFS= read -r line; do log_info " > $line" done if [[ ${exit_code} -eq 0 ]]; then log_info "Command succeeded on attempt $attempt" rm -f "${output_file}" echo "$command_output" return 0 else # Log detailed error information log_warn "Command failed (attempt $attempt, exit code: ${exit_code})" if (( attempt == max_attempts )); then log_error "Maximum attempts reached for command, last exit code: ${exit_code}" log_error "Last error output (first 10 lines):" head -n 10 "${output_file}" | while IFS= read -r line; do log_error " > $line" done rm -f "${output_file}" echo "$command_output" return ${exit_code} fi # Exponential backoff - wait longer for each successive attempt local adjusted_wait=$((wait_time * attempt)) log_info "Waiting ${adjusted_wait} seconds before retry" sleep ${adjusted_wait} (( attempt++ )) fi done rm -f "${output_file}" echo "$command_output" 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=("$@") local error_count=0 for dir in "${directories[@]}"; do # Skip empty directory paths if [[ -z "${dir}" ]]; then continue fi if [[ ! -d "${dir}" ]]; then log_error "Directory missing: ${dir}" error_count=$((error_count + 1)) continue fi if [[ ! -w "${dir}" ]]; then log_warn "Write permission denied for: ${dir}" log_warn "This may cause problems - the script will continue but operations may fail" # Don't increment error_count to allow script to continue fi done if [[ ${error_count} -gt 0 ]]; then log_error "${error_count} required directories are missing" return 1 fi return 0 }