#!/bin/bash
#Copyright 2005-2024, Lime Technology, Inc.
#License: GPLv2 only

# This is the 'mover' script used for moving files between pools and/or the main unRAID array.
# It is typically invoked via cron.

# The script is set up so that hidden directories (i.e., directory names beginning with a '.'
# character) at the topmost level of a pool or an array disk are not moved.  This behavior
# can be turned off by uncommenting the following line:
# shopt -s dotglob

# Files at the top level of a pool or an array disk are never moved.

# The 'find' command generates a list of all files and directories of a share.
# For each file, if the file is not "in use" by any process (as detected by 'in_use' command),
# then the file is moved, and upon success, deleted from the source disk.  If the file already
# exists on the target, it is not moved and the source is not deleted.  All meta-data of moved
# files/directories is preserved: permissions, ownership, extended attributes, and access/modified
# timestamps.

# If an error occurs in copying a file, the partial file, if present, is deleted and the
# operation continues on to the next file.

PIDFILE="/var/run/mover.pid"
CFGPATH="/boot/config/shares"
DEBUGGING=""

move() {
  find "$1" -depth 2>/dev/null | /usr/libexec/unraid/move $2 $DEBUGGING
  # second pass to clean up leftover empty directories
  find "$1" -depth -type d 2>/dev/null | /usr/libexec/unraid/move $2 $DEBUGGING
}

start() {
  if [ -f $PIDFILE ]; then
    if ps h $(cat $PIDFILE) | grep mover ; then
        echo "mover: already running"
        exit 1
    fi
  fi

  echo $$ >/var/run/mover.pid
  echo "mover: started"

  shopt -s nullglob

  for SHARECFG in $CFGPATH/*.cfg ; do
    SHARE=$(basename "$SHARECFG" .cfg)
    source <(fromdos < "$SHARECFG")

    # maybe move from primary to secondary
    if [[ $shareUseCache = yes && -n $shareCachePool && -d "/mnt/$shareCachePool/$SHARE" ]]; then
      if [[ -n $shareCachePool2 ]]; then
        # secondary is a pool
        if [[ -d "/mnt/$shareCachePool2" ]]; then
          move "/mnt/$shareCachePool/$SHARE"
        fi
      else
        # secondary is the unRAID array, ensure at least one data disk is present
        disks=(/mnt/disk[1-9]/ /mnt/disk[1-9][0-9]/)
        if [[ ${#disks[@]} -gt 0 ]]; then
          move "/mnt/$shareCachePool/$SHARE"
        fi
      fi
    fi

    # maybe move from secondary to primary
    if [[ $shareUseCache = prefer ]]; then
      if [[ -n $shareCachePool2 ]]; then
        # secondary is a pool (primary is also a pool)
        if [[ -d "/mnt/$shareCachePool2/$SHARE" && -n $shareCachePool && -d "/mnt/$shareCachePool" ]]; then
          move "/mnt/$shareCachePool2/$SHARE"
        fi
      else
        # secondary is the unRAID array
        if [[ -n $shareCachePool && -d "/mnt/$shareCachePool" ]]; then
          for SHAREPATH in /mnt/disk[1-9]/"$SHARE"/ /mnt/disk[1-9][0-9]/"$SHARE"/ ; do
            move "$SHAREPATH"
          done
        fi
      fi
    fi
  done

  # maybe empty an array disk
  EMPTYING=$(grep '^shareUserEmptying=' /boot/config/share.cfg | cut -d'"' -f2)
  IFS=',' read -ra arr <<< "$EMPTYING"
  for DISK in "${arr[@]}"; do
    # we can only empty share directories
    for SHAREPATH in /mnt/$DISK/* ; do
      SHARE=$(basename "$SHAREPATH")
      if [[ -d "$SHAREPATH" && -f "$CFGPATH/$SHARE.cfg" ]]; then
        move "$SHAREPATH" "-e"
      fi
    done

    # output list of files which could not be moved
    # use 'find' in case huge number of files left in /mnt/$DISK
    count=$(find /mnt/$DISK -mindepth 1 | wc -l)
    if [ "$count" -gt 0 ]; then
      find /mnt/$DISK -mindepth 1 -depth -printf 'move: %p Not moved\n' | head -n 100
      if [ "$count" -gt 100 ]; then
        echo "[output truncated to first 100 entries]"
      fi
    fi
  done

  rm -f $PIDFILE
  echo "mover: finished"
}

empty() {
  DISK="$1"

  if [ -f $PIDFILE ]; then
    if ps h $(cat $PIDFILE) | grep mover ; then
        echo "mover: already running"
        exit 1
    fi
  fi

  echo $$ >/var/run/mover.pid
  echo "mover: started"

  shopt -s nullglob

  # we can only empty share directories
  for SHAREPATH in /mnt/$DISK/* ; do
    SHARE=$(basename "$SHAREPATH")
    if [[ -d "$SHAREPATH" && -f "$CFGPATH/$SHARE.cfg" ]]; then
      move "$SHAREPATH" "-e"
    fi
  done

  # output list of files which could not be moved
  # use 'find' in case huge number of files left in /mnt/$DISK
  count=$(find /mnt/$DISK -mindepth 1 | wc -l)
  if [ "$count" -gt 0 ]; then
    find /mnt/$DISK -mindepth 1 -depth -printf 'move: %p Not moved\n' | head -n 100
    if [ "$count" -gt 100 ]; then
      echo "[output truncated to first 100 entries]"
    fi
  fi

  rm -f $PIDFILE
  echo "mover: finished"
}

killtree() {
  local pid=$1 child
    
  for child in $(pgrep --ns $$ -P $pid); do
    killtree $child
  done
  [ $pid -ne $$ ] && kill -TERM $pid
}

stop() {
  if [ ! -f $PIDFILE ]; then
    echo "mover: not running"
    exit 0
  fi
  killtree $(cat $PIDFILE)
  sleep 2
  rm -f $PIDFILE
  echo "mover: stopped"
}

# display usage and then exit
usage() {
    echo "Usage: $0 start [-e <disk_name>]"
    echo "       $0 stop|status"
    echo "       <disk_name> must match pattern 'disk[0-9]*' and /mnt/<disk_name> must be a mountpoint"
    exit 1
}

# validate disk name
validate_disk() {
  if [[ ! "$1" =~ ^disk[0-9]+$ ]]; then
    echo "Error: <disk_name> must match pattern 'disk[0-9]+$'"
    usage
  fi
  if ! mountpoint --nofollow /mnt/$1 > /dev/null 2>&1; then
    echo "Error: nothing mounted at /mnt/$1"
    usage
  fi
}

if [ "$#" -lt 1 ]; then
    usage
    exit 1
fi

case $1 in
start)
    if [ -z "$2" ]; then
        start
    elif [ "$2" == "-e" ]; then
        if [ -z "$3" ]; then
            usage
        else
            validate_disk "$3"
            empty "$3"
        fi
    else
        usage
    fi
  ;;
stop)
  stop
  ;;
status)
  [ -f $PIDFILE ]
  ;;
*)
  usage
  ;;
esac
