#!/bin/bash
#
# shellcheck disable=SC1091,SC2086,SC2181
#
# Copyright (c) 2013-2026 openmediavault plugin developers
#
# This file is licensed under the terms of the GNU General Public
# License version 3. This program is licensed "as is" without any
# warranty of any kind, whether express or implied.
#
# version: 1.4.0
#

. /etc/default/openmediavault
. /usr/share/openmediavault/scripts/helper-functions

declare -i esp=0
declare -i threads=1
declare -a extra
declare -a zstd_opts

arch=$(uname --machine)
threads=$(nproc --all | awk '{print ($1 > 32) ? 32 : $1}')

# logging location
logDir="/var/log/"
logFile="${logDir}/omv-backup.log"

# Shim to write messages to stdout and to the logfile.
_log_export()
{
  echo "[$(date +'%Y-%m-%d %H:%M:%S%z')] [backup] ${msg}" | tee -a ${logFile} >&2
}

_log()
{
  msg=${1}
  _log_export "$(omv_log "${msg}")"
}

# Level for recoverable errors.
_log_warning()
{
  msg=${1}
  _log_export "$(omv_warning "${msg}")"
}

# Level for unrecoverable errors that would be write to stderr an to the logfile.
_log_error()
{
  msg=${1}
  echo "[$(date +'%Y-%m-%d %H:%M:%S%z')] [backup] $(omv_error "${msg}" 2>&1)" > >(tee -a ${logFile} >&2)
}

purgeOld()
{
  keep=$(omv_config_get "/config/system/backup/keep")
  keepmode=$(omv_config_get "/config/system/backup/keepmode")
  [ -n "${keepmode}" ] || keepmode="days"
  _log "keep :: ${keep} (mode: ${keepmode})"
  if [[ ${keep} -le 0 ]]; then
    _log "Purging disabled."
    return
  fi
  if [ "${keepmode}" = "count" ]; then
    # Keep the N most recent backup sets. Delegate to the purge helper, which
    # already implements per-method count retention (borg --keep-last + compact,
    # dd/ddfull/fsarchiver set-counting, rsync no-op) and sidecar cleanup.
    _log "Purging old backups, keeping the ${keep} most recent set(s) ..."
    /usr/sbin/omv-backup-purge --keep "${keep}" 2>&1 | tee -a "${logFile}" >&2
    if [ ${PIPESTATUS[0]} -eq 0 ]; then
      _log "Purging done."
    else
      _log_warning "Purge failed!"
    fi
  else
    _log "Purging backup files older than ${keep} day(s) ..."
    find "${backupDir}" -maxdepth ${OMV_BACKUP_MAX_DEPTH} -type f -mtime +${keep} -name "${OMV_BACKUP_FILE_PREFIX}*" -delete
    if [ $? -eq 0 ]; then
      _log "Purging done."
    else
      _log_warning "Purge failed!"
    fi
  fi
}

gen_opts_array() {
  local -n opts_array="$1"
  local input="$2"
  eval set -- $input
  opts_array=("$@")
}

_log "Starting backup ..."
_log "threads :: ${threads}"
_log "arch :: ${arch}"

# date
date=$(date +"%Y-%m-%d_%H-%M-%S")

# clean apt-get cache to save space
apt-get clean

# clean openmediavault upload cache and recreate Packages file
rm -f /var/cache/openmediavault/archives/*
touch /var/cache/openmediavault/archives/Packages

# Get the shared folder reference and path
sfref=$(omv_config_get "/config/system/backup/sharedfolderref")
if ! omv_isuuid "${sfref}"; then
  _log_error "No backup volume set.  Please choose a backup volume."
  exit 10
fi
sfpath="$(omv_get_sharedfolder_path "${sfref}")"
if [ ! -d "${sfpath}" ]; then
  _log_error "Shared folder directory does not exist.  Exiting."
  exit 11
fi

# get verbose setting
verbose=$(omv_config_get "/config/system/backup/verbose")

# set backup directory and create it if it doesn't exist
backupDir="${sfpath}/${OMV_BACKUP_DIR_NAME}"
_log "Create ${backupDir}"
mkdir -p "${backupDir}"

# Get the method of backup
method=$(omv_config_get "/config/system/backup/method")
_log "Method: ${method}"

# get device file for /
devicefile=$(awk '$5 == "/" { print $10 }' /proc/self/mountinfo)
_log "Device file:  ${devicefile}"

# try alternate method to get root device if /dev/root is returned
if [ "${devicefile}" = "/dev/root" ]; then
  devicefile=$(findmnt -n / | awk '{ print $2 }')
fi

if [ "${devicefile}" = "/dev/root" ] || [ -z "${devicefile}" ]; then
  _log_error "Could not determine root device file. Please specify in the root device textbox. Exiting."
  exit 12
fi

# get root device
root=$(omv_config_get "/config/system/backup/root")

# if root is empty, try to determine root device
if [ -z "${root}" ]; then
  root="/dev/$(lsblk -no pkname ${devicefile})"
else
  _log "Using actual device for ${root}"
  root="$(readlink -f "${root}")"
fi
_log "Root drive: ${root}"

if [ -z "${root}" ]; then
  _log_error "Could not determine root device.  Exiting."
  exit 13
fi

# save helpful information
_log "Save fdisk output for ${root}"
fdisk --list ${root} > "${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}.fdisk"
if [ $? -eq 0 ]; then
  _log "Successfully saved fdisk output for ${root}"
else
  _log_warning "Save of fdisk output for ${root} failed!"
fi
_log "Save blkid output"
blkid > "${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}.blkid"
if [ $? -eq 0 ]; then
  _log "Successfully saved blkid output"
else
  _log_warning "Save of blkid output failed!"
fi
_log "Save package list"
dpkg --list | grep openmediavault > "${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}.packages"
if [ $? -eq 0 ]; then
  _log "Successfully saved package list"
else
  _log_warning "Save of package list failed!"
fi

# zstd profile selector
if [ -z "${OMV_BACKUP_ZSTD_OPTIONS}" ]; then
  if [ "${threads}" -eq 1 ] && [[ "${arch}" = "armv"* ]]; then
    OMV_BACKUP_ZSTD_OPTIONS="--fast=5"
  elif [[ "${arch}" = "armv"* ]]; then
    OMV_BACKUP_ZSTD_OPTIONS="-T0 --fast"
  elif [ ${OMV_BACKUP_ZSTD_ADAPT} -eq 1 ]; then
    OMV_BACKUP_ZSTD_OPTIONS="-T0 --adapt --long"
  else
    OMV_BACKUP_ZSTD_OPTIONS="-T0 --long"
  fi
fi

gen_opts_array zstd_opts "${OMV_BACKUP_ZSTD_OPTIONS}"

# calculate partition table size to accommodate GPT and MBR.
part_type=$(blkid --probe ${root} | cut -d \" -f4)
_log "Partition type :: ${part_type}"
if [ "${part_type}" = "gpt" ]; then
  num_parts=$(parted -m ${root} print | tail -n1 | cut -b1)
  grubparts_bs_calc=$(((128 * num_parts) + 1024))
  esp=$(parted -m ${root} print | awk -F ":" '$7 ~ /esp/ { print $1 }')
  if [ ${esp} -lt 1 ]; then
    _log "ESP partition not found."
  else
    partletter=""
    if [[ ${root} =~ nvme ]] || [[ ${root} =~ mmcblk ]]; then
      partletter="p"
    fi
    esppart="${root}${partletter}${esp}"
    _log "ESP partition :: ${esppart}"
    if [ -e "${esppart}" ]; then
      # add verbose flag if verbose is enabled
      vb=""
      if [ ${verbose} -eq 1 ]; then
        vb="--verbose"
      fi
      fstrim /boot/efi
      _log "Backup ESP partition"
      zstd "${zstd_opts[@]}" ${vb} -o "${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}.espdd.zst" < "${esppart}"
      if [ $? -eq 0 ]; then
        _log "Successfully backuped ESP partition"
      else
        _log_warning "Backup of ESP partition failed!"
      fi
    else
      _log "ESP partition '${esppart}' not found."
    fi
  fi
else
  grubparts_bs_calc=512
fi


# save partition table and mbr
_log "Save mbr"
dd if=${root} of="${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}.grub" bs=446 count=1
if [ $? -eq 0 ]; then
  _log "Successfully saved MBR"
else
  _log_warning "Save of MBR failed!"
fi
_log "Save mbr and partition table"
dd if=${root} of="${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}.grubparts" bs=${grubparts_bs_calc} count=1
if [ $? -eq 0 ]; then
  _log "Successfully saved MBR and partition table"
else
  _log_warning "Save of MBR and partition table failed!"
fi

# Save partition table using sfdisk (MBR) or sgdisk (GPT)
if [ "${part_type}" = "gpt" ]; then
  _log "Save partitions using sgdisk"
  sgdisk --backup="${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}.sgdisk" ${root}
  if [ $? -eq 0 ]; then
    _log "Successfully saved partition table using sgdisk"
  else
    _log_warning "Save of sgdisk partition table failed"
  fi
else
  _log "Save partitions using sfdisk"
  sfdisk --dump ${root} > "${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}.sfdisk"
  if [ $? -eq 0 ]; then
    _log "Successfully saved partition table using sfdisk"
  else
    _log_warning "Save of sfdisk partition table failed"
  fi
fi

# check for /boot partition
bootpart=$(awk '$2 == "/boot" || $2 == "/boot/firmware" { print $1; exit }' /proc/mounts)
if [ ! -b "${bootpart}" ]; then
  bootpart=""
else
  _log "Boot drive: ${bootpart}"
fi

# backup u-boot if platform_install.sh exists
if [ -f "/usr/lib/u-boot/platform_install.sh" ]; then
  . /usr/lib/u-boot/platform_install.sh
  if [ -d "${DIR}" ]; then
    _log "Backup u-boot"
    tar --create --zstd --file "${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}_u-boot.tar.zst" ${DIR}/*
    if [ $? -eq 0 ]; then
      _log "Successfully backuped u-boot"
    else
      _log_warning "Backup of u-boot failed!"
    fi
  fi
fi

# perform backup based on method selected
case ${method} in
  dd)
    vb=""
    if [ ${verbose} -eq 1 ]; then
      vb="--verbose"
    else
      vb="--quiet"
    fi
    _log "Running fstrim on / ..."
    fstrim /
    _log "Starting dd backup ..."
    zstd "${zstd_opts[@]}" ${vb} -v -o "${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}.dd.zst" <"${devicefile}"
    if [[ $? -gt 0 ]]; then
      _log_error "zstd exit code = $?"
      _log_error "dd backup failed!"
      exit 14
    else
      _log "dd backup complete."
    fi
    sync
    if [ -n "${bootpart}" ]; then
      _log "Running fstrim on /boot ..."
      fstrim /boot/
      _log "Starting dd backup of boot partition ..."
      zstd "${zstd_opts[@]}" ${vb} -o "${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}_boot.dd.zst" <"${bootpart}"
      if [ $? -eq 0 ]; then
        _log "dd backup of boot partition complete."
      else
        _log_error "zstd exit code = $?"
        _log_error "dd backup of boot partition failed!"
        exit 15
      fi
    fi
    sync
    touch "${backupDir}/${OMV_BACKUP_FILE_PREFIX}"-${date}*.dd.zst
    purgeOld
    ;;

  ddfull)
    vb=""
    if [ ${verbose} -eq 1 ]; then
      vb="--verbose"
    else
      vb="--quiet"
    fi
    for mp in $(lsblk --noheadings --output MOUNTPOINT ${root}); do
      _log "Running fstrim on ${mp} ..."
      fstrim "${mp}"
    done
    _log "Starting dd full disk ..."
    zstd "${zstd_opts[@]}" ${vb} -o "${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}.ddfull.zst" < "${root}"
    if [[ $? -gt 0 ]]; then
      _log_error "zstd exit code = $?"
      _log_error "dd full disk backup failed!"
      exit 16
    else
      _log "dd full disk backup complete."
    fi
    sync
    sync
    touch "${backupDir}/${OMV_BACKUP_FILE_PREFIX}"*-${date}.ddfull.zst
    purgeOld
    ;;

  fsarchiver)
    # add progress flag if verbose is enabled
    vb=""
    if [ ${verbose} -eq 1 ]; then
      vb="-v"
    fi
    _log "Starting FSArchiver backup ..."
    gen_opts_array extra "$(omv_config_get "/config/system/backup/extraoptions")"
    passwd="$(omv_config_get "/config/system/backup/passwd")"
    password=""
    if [ -n "${passwd}" ]; then
      _log "Encrypting archive ..."
      password="--cryptpass=${passwd}"
    fi
    fsarchiver savefs ${password} -o "${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}.fsa" ${devicefile} ${bootpart} ${vb} -A -Z ${OMV_BACKUP_FSA_COMP_LEVEL} -j ${threads} "${extra[@]}"
    if [ $? -ne 0 ]; then
      _log_error "FSArchiver backup failed!"
      exit 17
    else
      _log "FSArchiver backup complete."
    fi
    touch "${backupDir}/${OMV_BACKUP_FILE_PREFIX}"-${date}.*
    purgeOld
    fsarchiver archinfo ${password} "${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}.fsa"
    ;;

  borg)
    _log "Starting borgbackup ..."
    gen_opts_array extra "$(omv_config_get "/config/system/backup/extraoptions")"
    passwd="$(omv_config_get "/config/system/backup/passwd")"
    if [ -n "${passwd}" ]; then
      _log "Encrypting archive ..."
      export BORG_PASSPHRASE="${passwd}"
    fi
    if [ ! -d "${backupDir}/borgbackup" ] ; then
      # First run - init borg directory
      _log "First run - init borg directory ..."
      if [ -n "${passwd}" ]; then
        borg init -e repokey "${backupDir}/borgbackup"
      else
        borg init -e none "${backupDir}/borgbackup"
      fi
    fi
    _log "Starting borgbackup create ..."
    # Capture borg's own output into the log so a failure shows the real reason
    # (e.g. "No space left on device", a repository/integrity error) instead of
    # just "create failed".  PIPESTATUS holds borg's exit code through the tee.
    borg create --stats "${backupDir}/borgbackup::${OMV_BACKUP_FILE_PREFIX}-${date}" / \
      -x --exclude-caches \
      -e "/dev" -e "/proc" -e "/sys" -e "/tmp" -e "/run" -e "/mnt" \
      -e "/media" -e "/lost+found" -e "/export" -e "/home/ftp" -e "/srv" "${extra[@]}" \
      2>&1 | tee -a "${logFile}" >&2
    rc=${PIPESTATUS[0]}
    # borg exit codes: 0 = success, 1 = warning (the archive is still created,
    # e.g. a file changed or could not be read during the run), >= 2 = error.
    if [ ${rc} -ge 2 ]; then
      _log_error "borgbackup create failed (borg exit code ${rc}). See the borg output above / in ${logFile}."
      exit 18
    elif [ ${rc} -eq 1 ]; then
      _log_warning "borgbackup create completed with warnings (borg exit code 1); some files may have changed or been unreadable during the backup."
    else
      _log "borgbackup create complete."
    fi
    keep=$(omv_config_get "/config/system/backup/keep")
    keepmode=$(omv_config_get "/config/system/backup/keepmode")
    [ -n "${keepmode}" ] || keepmode="days"
    if [[ ${keep} -gt 0 ]]; then
      purgeOld
      # In count mode purgeOld already pruned the repo (--keep-last), compacted
      # and swept sidecars via omv-backup-purge; only the age-based prune is done
      # inline here.
      if [ "${keepmode}" != "count" ]; then
        _log "Starting borgbackup prune ..."
        borg prune "${backupDir}/borgbackup" --keep-daily "${keep}" 2>&1 | tee -a "${logFile}" >&2
        rc=${PIPESTATUS[0]}
        if [ ${rc} -ge 2 ]; then
          _log_error "borgbackup prune failed (borg exit code ${rc}). See the borg output above / in ${logFile}."
          exit 19
        else
          _log "borgbackup prune complete."
        fi
      fi
    fi
    ;;

  rsync)
    # add progress flag if verbose is enabled
    vb=""
    if [ ${verbose} -eq 1 ]; then
      vb="v"
    fi
    _log "Starting rsync ..."
    gen_opts_array extra "$(omv_config_get "/config/system/backup/extraoptions")"

    rsync -aAX${vb}xx /* "${backupDir}/" \
      --delete \
      --exclude=/dev \
      --exclude=/proc \
      --exclude=/sys \
      --exclude=/tmp \
      --exclude=/run \
      --exclude=/mnt \
      --exclude=/media \
      --exclude=/lost+found \
      --exclude=/export \
      --exclude=/home/ftp \
      --exclude=/srv "${extra[@]}"
    if [ $? -ne 0 ]; then
      _log_error "rsync backup failed!"
      exit 20
    else
      _log "rsync backup complete."
    fi
    purgeOld
    ;;
esac

# Save a machine-readable manifest describing this backup so that
# omv-restore-helper can perform a guided/automated restore without having to
# guess the disk layout.  Values are shell-quoted; consumers should parse with
# grep and must NOT source the file.
_log "Save restore manifest"
manifestFile="${backupDir}/${OMV_BACKUP_FILE_PREFIX}-${date}.manifest"
pfx="${OMV_BACKUP_FILE_PREFIX}-${date}"

# partition name infix ("p" for nvme/mmcblk devices, e.g. mmcblk0p1, nvme0n1p1)
partletter=""
if [[ ${root} =~ nvme ]] || [[ ${root} =~ mmcblk ]]; then
  partletter="p"
fi

# board model (arm/SBC) if exposed by the device tree
board_model=""
if [ -f /proc/device-tree/model ]; then
  board_model=$(tr -d '\0' < /proc/device-tree/model)
fi

# u-boot present? (relevant for SBC restore - no grub/chroot needed)
uboot_present=0
[ -f "/usr/lib/u-boot/platform_install.sh" ] && uboot_present=1

# archive encrypted? (fsarchiver/borg with a passphrase set)
encrypted=0
[ -n "$(omv_config_get "/config/system/backup/passwd")" ] && encrypted=1

# UUIDs (best effort - aids fstab fixups after a file-level restore)
root_fs_uuid=$(blkid -s UUID -o value "${devicefile}" 2>/dev/null)
boot_uuid=""
[ -n "${bootpart}" ] && boot_uuid=$(blkid -s UUID -o value "${bootpart}" 2>/dev/null)
esp_uuid=""
[ -n "${esppart}" ] && esp_uuid=$(blkid -s UUID -o value "${esppart}" 2>/dev/null)

# append an escaped key="value" line to the manifest
_mf() { printf '%s="%s"\n' "${1}" "${2//\"/}" >> "${manifestFile}"; }

{
  echo "# OpenMediaVault Backup Manifest"
  echo "# Machine-readable metadata for omv-restore-helper."
  echo "# Values are shell-quoted; parse with grep (do not source)."
} > "${manifestFile}"

_mf "manifest_version" "1"
_mf "backup_prefix"    "${OMV_BACKUP_FILE_PREFIX}"
_mf "backup_date"      "${date}"
_mf "created"          "$(date +'%Y-%m-%dT%H:%M:%S%z')"
_mf "hostname"         "$(hostname 2>/dev/null)"
_mf "omv_version"      "$(dpkg-query -W -f='${Version}' openmediavault 2>/dev/null)"
_mf "method"           "${method}"
_mf "arch"             "${arch}"
_mf "board_model"      "${board_model}"
_mf "uboot_present"    "${uboot_present}"
_mf "encrypted"        "${encrypted}"
_mf "root_disk"        "${root}"
_mf "root_fs_device"   "${devicefile}"
_mf "root_fs_uuid"     "${root_fs_uuid}"
_mf "part_type"        "${part_type}"
_mf "part_letter"      "${partletter}"
_mf "esp_part_number"  "${esp}"
_mf "esp_device"       "${esppart}"
_mf "esp_uuid"         "${esp_uuid}"
_mf "boot_device"      "${bootpart}"
_mf "boot_uuid"        "${boot_uuid}"

# record which artifact files were actually produced for this backup set
declare -A artifacts=(
  [file_dd]="${pfx}.dd.zst"
  [file_boot]="${pfx}_boot.dd.zst"
  [file_ddfull]="${pfx}.ddfull.zst"
  [file_espdd]="${pfx}.espdd.zst"
  [file_fsa]="${pfx}.fsa"
  [file_sfdisk]="${pfx}.sfdisk"
  [file_sgdisk]="${pfx}.sgdisk"
  [file_grub]="${pfx}.grub"
  [file_grubparts]="${pfx}.grubparts"
  [file_uboot]="${pfx}_u-boot.tar.zst"
  [file_fdisk]="${pfx}.fdisk"
  [file_blkid]="${pfx}.blkid"
  [file_packages]="${pfx}.packages"
)
for key in "${!artifacts[@]}"; do
  [ -f "${backupDir}/${artifacts[${key}]}" ] && _mf "${key}" "${artifacts[${key}]}"
done

if [ "${method}" = "borg" ] && [ -d "${backupDir}/borgbackup" ]; then
  _mf "borg_repo"    "borgbackup"
  _mf "borg_archive" "${pfx}"
fi
if [ "${method}" = "rsync" ]; then
  _mf "rsync_root" "."
fi

if [ -s "${manifestFile}" ]; then
  _log "Successfully saved restore manifest"
else
  _log_warning "Save of restore manifest failed!"
fi

_log "Copy restore helper script to backup"
cp -fv /usr/sbin/omv-restore-helper "${backupDir}/"

_log "Generate restore README"
/usr/sbin/omv-restore-helper readme "${backupDir}" "${date}" "${backupDir}/RESTORE-README.txt" >/dev/null 2>&1
if [ $? -eq 0 ]; then
  _log "Successfully generated restore README"
else
  _log_warning "Generation of restore README failed!"
fi

_log "Backup complete."

exit 0
