2025-03-04 09:01:59 +00:00

245 lines
7.6 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 Transmissionreported path into the local path.
translate_source() {
local src="$1"
echo "${src/#${TRANSMISSION_PATH_PREFIX}/${LOCAL_PATH_PREFIX}}"
}
# parse_args: Processes commandline 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=("$@")
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
}