229 lines
7.2 KiB
Bash
229 lines
7.2 KiB
Bash
#!/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
|
||
} |