#!/usr/bin/env bash set -euo pipefail CONFIG_URL_DEFAULT="https://opengist.resetrix.work/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/thunderbird-backup.conf" CONFIG_FILE="${THUNDERBIRD_BACKUP_CONFIG:-}" CONFIG_URL="${THUNDERBIRD_BACKUP_CONFIG_URL:-$CONFIG_URL_DEFAULT}" SHOW_HELP=false args=("$@") for ((i = 0; i < ${#args[@]}; i++)); do case "${args[$i]}" in --config) CONFIG_FILE="${args[$((i + 1))]:?Missing value for --config}" CONFIG_URL="" ;; --config-url) CONFIG_URL="${args[$((i + 1))]:?Missing value for --config-url}" CONFIG_FILE="" ;; -h|--help) SHOW_HELP=true ;; esac done SOURCE_DEFAULT="/home/vernon/.thunderbird/" DESTINATION_DEFAULT="/mnt/Synology/Backup/Applications/Thunderbird/" MOUNTPOINT_DEFAULT="/mnt/Synology/Backup" LOG_FILE_DEFAULT="/home/vernon/.local/state/thunderbird-backup.log" LOCK_FILE_DEFAULT="/home/vernon/.cache/thunderbird-backup.lock" DRY_RUN=false SOURCE="$SOURCE_DEFAULT" DESTINATION="$DESTINATION_DEFAULT" MOUNTPOINT="$MOUNTPOINT_DEFAULT" LOG_FILE="$LOG_FILE_DEFAULT" LOCK_FILE="$LOCK_FILE_DEFAULT" trim() { local value="$1" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" printf '%s' "$value" } parse_config_content() { local line key raw value while IFS= read -r line || [[ -n "$line" ]]; do line="$(trim "$line")" [[ -z "$line" || "$line" == \#* ]] && continue if [[ ! "$line" =~ ^([A-Z_]+)[[:space:]]*=(.*)$ ]]; then echo "ERROR: Invalid config line: $line" >&2 exit 2 fi key="${BASH_REMATCH[1]}" raw="$(trim "${BASH_REMATCH[2]}")" if [[ "$raw" == \"*\" && "$raw" == *\" ]]; then value="${raw:1:${#raw}-2}" elif [[ "$raw" == \'*\' && "$raw" == *\' ]]; then value="${raw:1:${#raw}-2}" else value="$raw" fi case "$key" in SOURCE) SOURCE="$value" ;; DESTINATION) DESTINATION="$value" ;; MOUNTPOINT) MOUNTPOINT="$value" ;; LOG_FILE) LOG_FILE="$value" ;; LOCK_FILE) LOCK_FILE="$value" ;; *) echo "ERROR: Unsupported config key: $key" >&2 exit 2 ;; esac done } usage() { cat <<'USAGE' Usage: backup-thunderbird-to-synology.sh [options] Options: --source PATH Thunderbird profile directory to back up --destination PATH Backup destination directory --mountpoint PATH Required mounted path to check before backup --no-mount-check Disable the mountpoint safety check --log-file PATH Log file path --config PATH Local config file path; disables remote config --config-url URL Remote config URL --dry-run Show what would change without writing files -h, --help Show this help Config defaults to: https://opengist.resetrix.work/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/thunderbird-backup.conf Supported config variables: SOURCE, DESTINATION, MOUNTPOINT, LOG_FILE, LOCK_FILE. Environment overrides: THUNDERBIRD_BACKUP_SOURCE, THUNDERBIRD_BACKUP_DESTINATION, THUNDERBIRD_BACKUP_MOUNTPOINT, THUNDERBIRD_BACKUP_LOG_FILE, THUNDERBIRD_BACKUP_LOCK_FILE, THUNDERBIRD_BACKUP_CONFIG, THUNDERBIRD_BACKUP_CONFIG_URL. USAGE } if [[ "$SHOW_HELP" == true ]]; then usage exit 0 fi if [[ -n "$CONFIG_FILE" ]]; then if [[ ! -f "$CONFIG_FILE" ]]; then mkdir -p "$(dirname "$LOG_FILE")" echo "$(date -Is) ERROR: Config file not found: $CONFIG_FILE" >> "$LOG_FILE" exit 1 fi parse_config_content < "$CONFIG_FILE" elif [[ -n "$CONFIG_URL" ]]; then config_content="$(curl -fsSL --max-time 20 "$CONFIG_URL")" || { mkdir -p "$(dirname "$LOG_FILE")" echo "$(date -Is) ERROR: Unable to fetch config from $CONFIG_URL" >> "$LOG_FILE" exit 1 } parse_config_content <<< "$config_content" fi SOURCE="${THUNDERBIRD_BACKUP_SOURCE:-$SOURCE}" DESTINATION="${THUNDERBIRD_BACKUP_DESTINATION:-$DESTINATION}" MOUNTPOINT="${THUNDERBIRD_BACKUP_MOUNTPOINT:-$MOUNTPOINT}" LOG_FILE="${THUNDERBIRD_BACKUP_LOG_FILE:-$LOG_FILE}" LOCK_FILE="${THUNDERBIRD_BACKUP_LOCK_FILE:-$LOCK_FILE}" while [[ $# -gt 0 ]]; do case "$1" in --source) SOURCE="${2:?Missing value for --source}" shift 2 ;; --destination) DESTINATION="${2:?Missing value for --destination}" shift 2 ;; --mountpoint) MOUNTPOINT="${2:?Missing value for --mountpoint}" shift 2 ;; --no-mount-check) MOUNTPOINT="" shift ;; --log-file) LOG_FILE="${2:?Missing value for --log-file}" shift 2 ;; --dry-run) DRY_RUN=true shift ;; --config) shift 2 ;; --config-url) shift 2 ;; -h|--help) usage exit 0 ;; *) echo "ERROR: Unknown option: $1" >&2 usage >&2 exit 2 ;; esac done mkdir -p "$(dirname "$LOG_FILE")" "$(dirname "$LOCK_FILE")" timestamp() { date -Is } normalize_path() { realpath -m "$1" } SOURCE_REAL="$(normalize_path "$SOURCE")" DESTINATION_REAL="$(normalize_path "$DESTINATION")" if [[ "$SOURCE" != /* || "$DESTINATION" != /* || "$LOG_FILE" != /* || "$LOCK_FILE" != /* ]]; then echo "$(timestamp) ERROR: SOURCE, DESTINATION, LOG_FILE, and LOCK_FILE must be absolute paths." >> "$LOG_FILE" exit 1 fi if [[ -n "$MOUNTPOINT" && "$MOUNTPOINT" != /* ]]; then echo "$(timestamp) ERROR: MOUNTPOINT must be an absolute path." >> "$LOG_FILE" exit 1 fi if [[ "$SOURCE_REAL" == "$DESTINATION_REAL" ]]; then echo "$(timestamp) ERROR: SOURCE and DESTINATION cannot be the same path: $SOURCE_REAL" >> "$LOG_FILE" exit 1 fi if [[ "$DESTINATION_REAL" == "$SOURCE_REAL"/* ]]; then echo "$(timestamp) ERROR: DESTINATION cannot be inside SOURCE: $DESTINATION_REAL" >> "$LOG_FILE" exit 1 fi case "$DESTINATION_REAL" in /|/home|/home/vernon|/mnt|/mnt/Synology|/mnt/Synology/Backup) echo "$(timestamp) ERROR: Refusing unsafe backup destination: $DESTINATION_REAL" >> "$LOG_FILE" exit 1 ;; esac if [[ -n "$MOUNTPOINT" ]] && ! mountpoint -q "$MOUNTPOINT"; then echo "$(timestamp) ERROR: $MOUNTPOINT is not mounted; backup skipped." >> "$LOG_FILE" exit 1 fi if [[ ! -d "$SOURCE" ]]; then echo "$(timestamp) ERROR: Thunderbird profile directory not found: $SOURCE" >> "$LOG_FILE" exit 1 fi mkdir -p "$DESTINATION" exec 9>"$LOCK_FILE" if ! flock -n 9; then echo "$(timestamp) INFO: Previous Thunderbird backup still running; backup skipped." >> "$LOG_FILE" exit 0 fi { if [[ "$DRY_RUN" == true ]]; then echo "$(timestamp) INFO: Starting Thunderbird backup dry run: $SOURCE_REAL -> $DESTINATION_REAL" else echo "$(timestamp) INFO: Starting Thunderbird backup: $SOURCE_REAL -> $DESTINATION_REAL" fi rsync_args=(-a --delete) if [[ "$DRY_RUN" == true ]]; then rsync_args+=(-n --itemize-changes) fi rsync "${rsync_args[@]}" \ --exclude='*.parentlock' \ --exclude='lock' \ "$SOURCE" "$DESTINATION" if [[ "$DRY_RUN" == true ]]; then echo "$(timestamp) INFO: Thunderbird backup dry run completed." else echo "$(timestamp) INFO: Thunderbird backup completed." fi } >> "$LOG_FILE" 2>&1