#!/bin/bash
#------------------------------------------------------------------------------
# MY_LIBRARY.sh - Utility functions for logging, environment detection, and file operations
#
# Description:
# A collection of utility functions used for logging messages, identifying the
# operating environment, managing file operations like safe deletion and linking
# directories, and handling crashed lock files for TV devices and shows. Designed
# to be sourced by other scripts for shared functionality.
#
# Globals:
# nEnvType - Integer representing the environment type (e.g., Cygwin, Linux) (RW)
# MIN_DISK_FREE_SPACE - Minimum disk free space threshold in bytes (RO)
# SAFE_DELETE_DIR - Directory for safe deletion of files (RO)
# DEBUG - Flag for enabling debug logging (RW)
# CALL_STDIN_FD - File descriptor for redirected stdin (RO)
# CALL_STDOUT_FD - File descriptor for redirected stdout (RO)
# CALL_STDERR_FD - File descriptor for redirected stderr (RO)
#
# Arguments:
# None (script is meant to be sourced)
#
# Outputs:
# Writes informational, warning, error, and debug messages to redirected stdout/stderr.
# Writes to rollback logs during safe deletion.
#
# Exit Codes:
# 0 - Success (functions generally return 0 on success)
# 1 - Failure (functions exit with 1 on error, e.g., missing directories, invalid arguments)
#
# Side Effects:
# Redirects stdin, stdout, and stderr to custom file descriptors.
# Creates directories and files for safe deletion and rollback logging.
# Deletes or moves files during safe deletion or lock removal.
# Modifies the environment variable `nEnvType` during environment detection.
# Executes system commands like `cmd.exe`, `ln`, `mv`, `rm`, `ps`, and `find`.
#------------------------------------------------------------------------------
# -1 : Invalid
# 1 : Cygwin
# 2 : Linux
# 3 : Linux WSL
# 4 : macOS
declare -i nEnvType=-1
declare -r -i MIN_DISK_FREE_SPACE=$(( 100 * 1024 * 1024 ))
declare -i DEBUG
declare SAFE_DELETE_DIR
: ${SAFE_DELETE_DIR:=/cygdrive/p/DELETED/}
: ${DEBUG:=0}
readonly SAFE_DELETE_DIR
readonly CALL_STDIN_FD=10
readonly CALL_STDOUT_FD=11
readonly CALL_STDERR_FD=12
# Redirect stdin, stdout, and stderr to custom FDs using file descriptor duplication
# Save the original file descriptors
if ! eval "exec ${CALL_STDIN_FD}<&0"; then
echo "ERROR: Failed to redirect stdin to FD ${CALL_STDIN_FD}" >&2
exit 1
fi
if ! eval "exec ${CALL_STDOUT_FD}>&1"; then
echo "ERROR: Failed to redirect stdout to FD ${CALL_STDOUT_FD}" >&2
exit 1
fi
if ! eval "exec ${CALL_STDERR_FD}>&2"; then
echo "ERROR: Failed to redirect stderr to FD ${CALL_STDERR_FD}" >&2
exit 1
fi
#==============================================================================
# EchoN - Outputs a message without a newline to redirected stdout
#------------------------------------------------------------------------------
# Description:
# Prints a formatted message with timestamp and program ID to the redirected
# stdout file descriptor without a trailing newline.
#
# Globals:
# sProgramId - Program identifier for logging (RW)
# CALL_STDOUT_FD - File descriptor for redirected stdout (RO)
#
# Arguments:
# $@ - Message to print
#
# Outputs:
# Writes formatted message to redirected stdout (CALL_STDOUT_FD).
#
# Exit Codes:
# None (function does not exit the script)
#
# Side Effects:
# None
#==============================================================================
EchoN() {
printf '%s|%-8s|%s' \
"$(date '+%Y-%m-%d %H:%M:%S')" \
"${sProgramId:-*UNSET*}" \
"$*" >&${CALL_STDOUT_FD}
}
#==============================================================================
# Echo - Outputs a message with a newline to redirected stdout
#------------------------------------------------------------------------------
# Description:
# Prints a formatted message with timestamp and program ID to the redirected
# stdout file descriptor, followed by a newline.
#
# Globals:
# sProgramId - Program identifier for logging (RW)
# CALL_STDOUT_FD - File descriptor for redirected stdout (RO)
#
# Arguments:
# $@ - Message to print
#
# Outputs:
# Writes formatted message with newline to redirected stdout (CALL_STDOUT_FD).
#
# Exit Codes:
# None (function does not exit the script)
#
# Side Effects:
# None
#==============================================================================
Echo() {
EchoN "$*"
printf '\n' >&${CALL_STDOUT_FD}
}
#==============================================================================
# EchoD - Outputs a debug message to redirected stderr
#------------------------------------------------------------------------------
# Description:
# Prints a debug message with a "[DEBUG]" prefix to the redirected stderr file
# descriptor.
#
# Globals:
# CALL_STDERR_FD - File descriptor for redirected stderr (RO)
#
# Arguments:
# $@ - Debug message to print
#
# Outputs:
# Writes debug message to redirected stderr (CALL_STDERR_FD).
#
# Exit Codes:
# None (function does not exit the script)
#
# Side Effects:
# None
#==============================================================================
EchoD() {
Echo "🛠 [DEBUG] $*" >&${CALL_STDERR_FD}
}
#==============================================================================
# EchoW - Outputs a warning message to redirected stdout
#------------------------------------------------------------------------------
# Description:
# Prints a warning message with a "[WARNING]" prefix to the redirected stdout
# file descriptor.
#
# Globals:
# CALL_STDOUT_FD - File descriptor for redirected stdout (RO)
#
# Arguments:
# $@ - Warning message to print
#
# Outputs:
# Writes warning message to redirected stdout (CALL_STDOUT_FD).
#
# Exit Codes:
# None (function does not exit the script)
#
# Side Effects:
# None
#==============================================================================
EchoW() {
Echo "⚠️ [WARNING] $*" >&${CALL_STDOUT_FD}
}
#==============================================================================
# EchoE - Outputs an error message to redirected stderr
#------------------------------------------------------------------------------
# Description:
# Prints an error message with a "[ERROR]" prefix to the redirected stderr file
# descriptor.
#
# Globals:
# CALL_STDERR_FD - File descriptor for redirected stderr (RO)
#
# Arguments:
# $@ - Error message to print
#
# Outputs:
# Writes error message to redirected stderr (CALL_STDERR_FD).
#
# Exit Codes:
# None (function does not exit the script)
#
# Side Effects:
# None
#==============================================================================
EchoE() {
Echo "❌ [ERROR] $*" >&${CALL_STDERR_FD}
}
#==============================================================================
# ExitWithMsg - Outputs an error message and exits the script
#------------------------------------------------------------------------------
# Description:
# Prints an error message to redirected stderr and exits the script with a
# failure status.
#
# Globals:
# CALL_STDERR_FD - File descriptor for redirected stderr (RO)
#
# Arguments:
# $@ - Error message to print
#
# Outputs:
# Writes error message to redirected stderr (CALL_STDERR_FD).
#
# Exit Codes:
# 1 - Failure (always exits with 1)
#
# Side Effects:
# Terminates the script execution.
# Enables `set -e` to ensure immediate exit on any subsequent error.
#==============================================================================
ExitWithMsg() {
set -e
echo >&${CALL_STDERR_FD}
EchoE "$*" >&${CALL_STDERR_FD}
exit 1
}
#==============================================================================
# IdentifyEnvironment - Detects the operating environment
#------------------------------------------------------------------------------
# Description:
# Identifies the operating environment (Cygwin, Linux, WSL, etc.) and sets the
# global `nEnvType` variable accordingly.
#
# Globals:
# nEnvType - Integer representing the environment type (RW)
#
# Arguments:
# None
#
# Outputs:
# None (sets global variable `nEnvType`)
#
# Exit Codes:
# 0 - Success (always returns 0)
#
# Side Effects:
# Modifies the global `nEnvType` variable based on the detected environment.
# Executes `uname` to determine the environment.
#==============================================================================
IdentifyEnvironment() {
if [[ "$(uname -o)" == Cygwin ]]; then
nEnvType=1
elif [[ "$(uname -r)" == *-microsoft-* ]]; then
nEnvType=3
elif [[ "$(uname -o)" == "GNU/Linux" ]]; then
nEnvType=2
else
nEnvType=-1
fi
}
#==============================================================================
# LinkDirectory - Creates a symbolic link between directories
#------------------------------------------------------------------------------
# Description:
# Creates a symbolic link from one directory to another, with platform-specific
# handling for Cygwin, WSL, and Linux environments.
#
# Globals:
# nEnvType - Integer representing the environment type (RW)
# DEBUG - Flag for enabling debug logging (RW)
#
# Arguments:
# $1 - Source directory path (where the link will be created)
# $2 - Target directory path (what the link points to)
#
# Outputs:
# Writes error messages to redirected stderr via `EchoE` if validation fails.
# Writes debug messages to redirected stderr via `EchoD` if DEBUG=1.
#
# Exit Codes:
# 1 - Failure (via ExitWithMsg on invalid arguments or environment)
#
# Side Effects:
# Calls `IdentifyEnvironment` if `nEnvType` is not set.
# Creates a symbolic link using `cmd.exe` (Cygwin/WSL) or `ln` (Linux).
# Executes external commands (`cmd.exe`, `ln`).
#==============================================================================
LinkDirectory() {
local -r sLink=""
local -r sTarget=""
# Validate arguments
[[ "$1" != "/cygdrive/"* || "$2" != "/cygdrive/"* ]] && ExitWithMsg "LinkDirectory: 1='$1' 2='$2'"
[[ ! -d "$2" ]] && ExitWithMsg "LinkDirectory: directory does not exist or is not accessible: '$2'"
# Ensure environment is identified
[[ ${nEnvType} -eq -1 ]] && IdentifyEnvironment
# 1 : Cygwin
# 2 : Linux
# 3 : Linux WSL
# 4 : macOS
case ${nEnvType} in
1|3) sLink="$(tr -s / <<< "${1:10:1}:${1:11}")"
sTarget="$(tr -s / <<< "${2:10:1}:${2:11}")"
cmd.exe <<< "mklink /D \"${sLink//\//\\\\}\" \"${sTarget//\//\\\\}\"" >/dev/null
(( DEBUG )) && EchoD "LinkDirectory: Created link '${sLink}' -> '${sTarget}' using cmd.exe"
;;
2) ln -s "$2" "$1" || EchoE "LinkDirectory: Failed to create link '$1' -> '$2'"
(( DEBUG )) && EchoD "LinkDirectory: Created link '$1' -> '$2' using ln"
;;
*) ExitWithMsg "LinkDirectory: Unsupported environment type: ${nEnvType}"
;;
esac
}
#==============================================================================
# SafeDelete - Safely moves a file or link to a trash directory with rollback logging
#------------------------------------------------------------------------------
# Description:
# Moves a file or symbolic link to a designated trash directory, renaming it with
# a timestamp and random suffix, and logs the operation for potential rollback.
#
# Globals:
# SAFE_DELETE_DIR - Directory for safe deletion of files (RO)
# lExecute - Flag to control execution (RW)
#
# Arguments:
# $1 - Source file or link to delete (prefix with '@' for quiet mode)
#
# Outputs:
# Writes informational messages to redirected stdout via `Echo` (unless quiet).
# Writes error messages to redirected stderr via `EchoE` on failure.
# Appends to rollback log file in SAFE_DELETE_DIR.
#
# Exit Codes:
# 0 - Success (file/link moved successfully)
# 1 - Failure (invalid arguments, missing directories, or move failure)
#
# Side Effects:
# Creates directories under SAFE_DELETE_DIR if they don’t exist.
# Moves the source file/link to SAFE_DELETE_DIR.
# Appends an entry to the rollback log file.
# Executes external commands (`mkdir`, `mv`).
#==============================================================================
SafeDelete() {
local -r sSourceFile="$1"
local -i lQuiet=0
local -r sBaseName=""
local -r sTimeStamp=""
local -r sNewName=""
local -r sCommand=""
# Validate SAFE_DELETE_DIR
[[ -z "${SAFE_DELETE_DIR}" ]] && ExitWithMsg "SAFE_DELETE_DIR is not defined. Cannot proceed"
# Determine if quiet mode is enabled and strip the '@' prefix if present
[[ "${sSourceFile:0:1}" == "@" ]] && lQuiet=1
[[ ${lQuiet} -eq 1 ]] && sSourceFile="${sSourceFile:1}"
# Validate source file
if [[ -z "${sSourceFile}" ]]; then
[[ ${lQuiet} -eq 0 ]] && EchoE "SafeDelete: No source item defined"
return 1
elif [[ ! -e "${sSourceFile}" && ! -L "${sSourceFile}" ]]; then
[[ ${lQuiet} -eq 0 ]] && EchoE "SafeDelete: '${sSourceFile}' does not exist"
return 1
fi
# Ensure trash directory exists
[[ ! -d "${SAFE_DELETE_DIR}" ]] && mkdir -p -- "${SAFE_DELETE_DIR}"
if [[ ! -d "${SAFE_DELETE_DIR}" ]]; then
[[ ${lQuiet} -eq 0 ]] && EchoE "SafeDelete: Trash bin '${SAFE_DELETE_DIR}' does not exist"
return 1
fi
# Prepare new filename with timestamp and random suffix
sBaseName="$(basename -- "${sSourceFile}")"
sTimeStamp="$(date +"%Y%m%d%H%M%s")"
sNewName="${sBaseName}_${sTimeStamp}_${RANDOM}"
# Log the operation (unless quiet)
[[ ${lQuiet} -eq 0 ]] && Echo " '${sSourceFile}' -> '${SAFE_DELETE_DIR}/${sNewName}'"
# Determine the command (echo or execute)
sCommand=$([[ ${lExecute} -eq 1 ]] && echo "" || echo "echo")
# Move the file or link
if [[ -L "${sSourceFile}" ]]; then
[[ ! -d "${SAFE_DELETE_DIR}/.Links" ]] && mkdir -p -- "${SAFE_DELETE_DIR}/.Links"
if [[ -d "${SAFE_DELETE_DIR}/.Links" ]]; then
${sCommand} mv -- "${sSourceFile}" "${SAFE_DELETE_DIR}/.Links/${sNewName}" || {
[[ ${lQuiet} -eq 0 ]] && EchoE "SafeDelete: Failed to move link '${sSourceFile}' to '${SAFE_DELETE_DIR}/.Links/${sNewName}'"
return 1
}
else
[[ ${lQuiet} -eq 0 ]] && EchoE "SafeDelete: Failed to create '${SAFE_DELETE_DIR}/.Links' directory"
return 1
fi
else
${sCommand} mv -- "${sSourceFile}" "${SAFE_DELETE_DIR}/${sNewName}" || {
[[ ${lQuiet} -eq 0 ]] && EchoE "SafeDelete: Failed to move file '${sSourceFile}' to '${SAFE_DELETE_DIR}/${sNewName}'"
return 1
}
fi
# Log the operation for rollback
if ! echo "${sTimeStamp}|${sSourceFile}|${SAFE_DELETE_DIR}/${sNewName}" >> "${SAFE_DELETE_DIR}/.RollBack.log"; then
[[ ${lQuiet} -eq 0 ]] && EchoE "SafeDelete: Failed to write to rollback log '${SAFE_DELETE_DIR}/.RollBack.log'"
return 1
fi
return 0
}
#==============================================================================
# RemoveCrashedLockTvDeviceAll - Removes crashed lock files for all TV devices
#------------------------------------------------------------------------------
# Description:
# Searches for '.IgnoreThisDeviceWhileMoving*' lock files under TV directories
# and removes them if the associated process is no longer running.
#
# Globals:
# None
#
# Arguments:
# $1 - Verbosity level (0 for quiet, >0 for verbose output)
#
# Outputs:
# Writes informational messages to redirected stdout via `Echo` if verbose.
#
# Exit Codes:
# 0 - Success (always returns 0)
#
# Side Effects:
# Deletes lock files if the associated process is not running.
# Executes external commands (`find`, `ps`, `rm`).
#==============================================================================
RemoveCrashedLockTvDeviceAll() {
local -r -i lVerbose="$1"
local sProcessId=""
(( lVerbose )) && Echo "RemoveCrashedLockTvDeviceAll"
(( lVerbose )) && Echo " Looking for '.IgnoreThisDeviceWhileMoving*' files under '/cygdrive/*/MediaLibraryExtension/TV/'..."
while IFS= read -r lock_file; do
[[ -z "${lock_file}" ]] && continue
(( lVerbose )) && Echo " '${lock_file}'"
sProcessId="${lock_file##*.}"
if [[ -n "${sProcessId}" ]]; then
(( lVerbose )) && Echo " Checking process id ${sProcessId}"
if ! ps -p "${sProcessId}" >/dev/null; then
rm "${lock_file}" && Echo " Removed"
fi
fi
done < <(
find /cygdrive/*/MediaLibraryExtension/TV/ -type f -iname '.IgnoreThisDeviceWhileMoving*' 2>/dev/null
)
(( lVerbose )) && Echo " Done"
}
#==============================================================================
# RemoveCrashedLockTvDevice - Removes crashed lock files for a specific TV device
#------------------------------------------------------------------------------
# Description:
# Removes '.IgnoreThisDeviceWhileMoving.*' lock files for a specific mount point
# if the associated process is no longer running.
#
# Globals:
# None
#
# Arguments:
# $1 - Mount point path to check for lock files
#
# Outputs:
# None
#
# Exit Codes:
# 0 - Success (always returns 0)
#
# Side Effects:
# Deletes lock files if the associated process is not running.
# Executes external commands (`ls`, `ps`, `rm`).
#==============================================================================
RemoveCrashedLockTvDevice() {
local -r sMountPoint="$1"
local -a processes=()
local nProcess=""
if [[ -n "${sMountPoint}" ]]; then
# Collect all process IDs from lock files
while IFS= read -r file; do
nProcess=$(basename "${file}" | cut -d. -f3)
[[ -n "${nProcess}" ]] && processes+=("${nProcess}")
done < <(
ls -1d "${sMountPoint}/MediaLibraryExtension/TV/.IgnoreThisDeviceWhileMoving."* 2>/dev/null
)
# Remove lock files for processes that are no longer running
for nProcess in "${processes[@]}"; do
if ! ps -p "${nProcess}" >/dev/null; then
rm -f "${sMountPoint}/MediaLibraryExtension/TV/.IgnoreThisDeviceWhileMoving.${nProcess}"
fi
done
fi
}
#==============================================================================
# RemoveCrashedLockShowAll - Removes crashed lock files for all TV shows
#------------------------------------------------------------------------------
# Description:
# Searches for '.IgnoreAlreadyRel*' lock files under TV directories and removes
# them if the associated process is no longer running.
#
# Globals:
# None
#
# Arguments:
# $1 - Verbosity level (0 for quiet, >0 for verbose output)
#
# Outputs:
# Writes informational messages to redirected stdout via `Echo` if verbose.
#
# Exit Codes:
# 0 - Success (always returns 0)
#
# Side Effects:
# Deletes lock files if the associated process is not running.
# Executes external commands (`find`, `ps`, `rm`).
#==============================================================================
RemoveCrashedLockShowAll() {
local -r -i lVerbose="$1"
local sProcessId=""
(( lVerbose )) && Echo "RemoveCrashedLockShowAll"
(( lVerbose )) && Echo " Looking for '.IgnoreAlreadyRel*' files under '/cygdrive/*/MediaLibraryExtension/TV/'..."
while IFS= read -r lock_file; do
[[ -z "${lock_file}" ]] && continue
(( lVerbose )) && Echo " '${lock_file}'"
sProcessId="${lock_file##*.}"
if [[ -n "${sProcessId}" ]]; then
(( lVerbose )) && Echo " Checking process id ${sProcessId}"
if ! ps -p "${sProcessId}" >/dev/null; then
rm "${lock_file}" && Echo " Removed"
fi
fi
done < <(
find /cygdrive/*/MediaLibraryExtension/TV/ -type f -iname '.IgnoreAlreadyRel*' 2>/dev/null
)
(( lVerbose )) && Echo " Done"
}
#==============================================================================
# RemoveCrashedLockShow - Removes crashed lock files for a specific TV show
#------------------------------------------------------------------------------
# Description:
# Removes '.IgnoreAlreadyRelocating.*' lock files for a specific source link
# if the associated process is no longer running.
#
# Globals:
# None
#
# Arguments:
# $1 - Source link path to check for lock files
#
# Outputs:
# None
#
# Exit Codes:
# 0 - Success (always returns 0)
#
# Side Effects:
# Deletes lock files if the associated process is not running.
# Executes external commands (`ls`, `ps`, `rm`).
#==============================================================================
RemoveCrashedLockShow() {
local -r sSourceLink="$1"
local -a processes=()
local nProcess=""
if [[ -n "${sSourceLink}" ]]; then
# Collect all process IDs from lock files
while IFS= read -r file; do
nProcess=$(basename "${file}" | cut -d. -f3)
[[ -n "${nProcess}" ]] && processes+=("${nProcess}")
done < <(
ls -1d "${sSourceLink}/.IgnoreAlreadyRelocating."* 2>/dev/null
)
# Remove lock files for processes that are no longer running
for nProcess in "${processes[@]}"; do
if ! ps -p "${nProcess}" >/dev/null; then
rm -f "${sSourceLink}/.IgnoreAlreadyRelocating.${nProcess}"
fi
done
fi
}
# Initialize the environment
IdentifyEnvironment
Comments
Post a Comment