Skip to main content

IT - Programming - BASH scripting - MY_LIBRARY.sh

#!/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

Popular posts from this blog

TV Series - The Brokenwood Mysteries [NZ] (2014) - Season 10

 

Movie - Sin City: A Dame to Kill For (2014)

 

Movies - Deadpool & Wolverine (2024)