#!/bin/bash
set -euo pipefail

# backup a web site directory and mysql
# tar web folder and mysqldump a database to archive folder, and scp to remote host
# 
# configure
# 1. change global config
# 2. add a site and set path to webroot
# 3. add mysql db to backup
# 4. config msmtp
#
# usage: 
#     bash <this-script.sh>             to archive web folder, 
#
#     bash <this-script.sh> --full      to ignore --exclude xxx 
#
# notice:  
#     require bash, and sh will NOT work
#
# Written with the assistance of ChatGPT and Gemini. 
# 2025/12/18
#
#




#######################################
# 全局配置
#######################################
TZ=Asia/Shanghai

NOW=$(TZ=${TZ} date +%Y%m%d-%H%M%S)

LOG_FILE=/home/username/auto_backup/log_${NOW}.log
LOCAL_SAVE=/home/username/auto_backup/save

# archive file kept limitation
KEEP_FULL=1
KEEP_BRF=5

# remote via ssh
REMOTE_USER=loginname
REMOTE_HOST=backup.path8.net
REMOTE_DIR='~/auto_backup_store'
SSH_KEY='/home/username/.ssh/id_rsa_auth_ssl'

# 自动通知到指定的邮件，每行一个，不需要任何分隔符号
MAIL_TO=(
	fengyqf@gmail.com
)		


# 是否启用排除
ENABLE_EXCLUDE=1   # 1=启用排除，0=完整备份

while [ "$#" -gt 0 ]; do
    case "$1" in
        --exclude) ENABLE_EXCLUDE=1 ;;
        --full) ENABLE_EXCLUDE=0 ;;
        *) ;;
    esac
    shift
done


touch $LOG_FILE
mkdir -p $LOCAL_SAVE

#######################################
# 工具函数
#######################################
log() {
    echo "[$(date '+%F %T')] $*" >> "$LOG_FILE"
}

remote_df() {
    ssh -4 -i "$SSH_KEY" "${REMOTE_USER}@${REMOTE_HOST}" df -h -x tmpfs -x devtmpfs >> "$LOG_FILE"  || log "WARN: ssh failed to retrieve remote disk usage"
}

send_report_mail() {
    {
        echo "Subject: auto backup report ${NOW}"
        echo
        cat "$LOG_FILE"
    } | msmtp "${MAIL_TO[@]}"
}

#######################################
# 核心备份函数
# 参数：
# 1: site_name, used as backup file name
# 2: source
# 3: dir,       dir in source, the real dir to be archived to .tar.gz, can be '.'
# 4: backup_dir
# 5: exclude_array_name
#######################################
backup_do() {
    local site="$1"
    local src="$2"
    local dir="$3"
    local outdir="$4"
    local exclude_arr_name="$5"

    mkdir -p "$outdir"

    local suffix
    local tar_opts=()

    if [[ "$ENABLE_EXCLUDE" -eq 1 ]]; then
        suffix="brf"
        local excludes=("${!exclude_arr_name}")
        for e in "${excludes[@]}"; do
            tar_opts+=(--exclude="$e")
        done
    else
        suffix="full"
    fi

    local tarfile="${outdir}/${site}_${NOW}_${suffix}.tar.gz"

    log "START backup site=${site} mode=${suffix}"

    if [[ "$ENABLE_EXCLUDE" -eq 1 ]]; then
        tar -czf "$tarfile" "${tar_opts[@]}" -C "$src" "$dir" 2>> "${LOG_FILE}" || true
    else
        tar -czf "$tarfile" -C "$src" "$dir" 2>> "${LOG_FILE}" || true
    fi
    chmod 664 "$tarfile"
    size_bytes=$(stat -c '%s' "$tarfile")
    size_human=$(du -h "$tarfile" | awk '{print $1}')

    scp -4 -i "$SSH_KEY" "$tarfile" \
        "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"  || log "WARN: scp failed for $tarfile"

    log "$(basename "$tarfile")  file size: ${size_bytes} bytes (${size_human})"

    cleanup_old "$outdir" "$site"

    # log ""
    # log "remote disk usage"
    # remote_df
    # log ""
    # log ""
}

#######################################
# 清理旧备份
#######################################
cleanup_old() {
    local dir="$1"
    local site="$2"


    shopt -s nullglob

    # -------- full backups --------
    local full_files=(
        "${dir}/${site}_"*_full.tar.gz
    )
    if ((${#full_files[@]} > KEEP_FULL)); then
        local to_delete=("${full_files[@]:KEEP_FULL}")
        for f in "${to_delete[@]}"; do
            rm -f -- "$f"
            log "_ _ _ removed: $(basename "$f")"
        done
    fi

    # -------- brief backups --------
    local brf_files=(
        "${dir}/${site}_"*_brf.tar.gz
    )
    if ((${#brf_files[@]} > KEEP_BRF)); then
        local to_delete=("${brf_files[@]:KEEP_BRF}")
        for f in "${to_delete[@]}"; do
            rm -f -- "$f"
            log "_ _ _ removed: $(basename "$f")"
        done
    fi

    shopt -u nullglob

}

#######################################
# suit for your web root 
# --------
# 待执行的备份项，及调用执行
# 
# each backup execution is called by backup_do(...) with the following 5 args:
#
# site_name="blog.path8.net"    # also used as prefix of ...tar.gz file-name
# site_src="/var/www/html/blog.path8.net"   # path your web
# site_dir="./html"                         # the dir in site_src to be archived
# cfg_excludes=(                            # exclude patterns
#    './html/upload'
# )
# savedir=$LOCAL_SAVE                       # save tar.gz to
# backup_do "$site_name" "$site_src" "$site_dir" "$savedir" cfg_excludes[@]
#######################################

#
site_name="blog.path8.net"
site_src="/var/www/html/blog.path8.net"
site_dir="./html"
cfg_excludes=(
    './html/wp-content/upgrade'
    './html/wp-content/wflogs'
    './html/wp-content/uploads/20[01]*'
    './html/wp-content/uploads/202[0-4]'
    './html/wp-content/uploads/2025/0[0-9]'
)
savedir=$LOCAL_SAVE
backup_do "$site_name" "$site_src" "$site_dir" "$savedir" cfg_excludes[@]



#
site_name="www.path8.net"
site_src="/var/www/html/www.path8.net"
site_dir="./html"
cfg_excludes=(
    './html/wp-content/upgrade'
    './html/wp-content/wflogs'
    './html/wp-content/uploads/20[01]*'
    './html/wp-content/uploads/202[0-4]'
    './html/wp-content/uploads/2025/0[0-9]'
)
savedir=$LOCAL_SAVE
backup_do "$site_name" "$site_src" "$site_dir" "$savedir" cfg_excludes[@]










##############################################################################
# extra backup mysql(mysqldump), mock above
# 
# /etc/my.cnf.d/backup_user.cnf
# user to run mysqldump,(at least: SELECT, LOCK TABLES, SHOW VIEW ) , eg.
# [client]
# user=db_backup
# password=backup-user-passwd
# host=localhost
# port=3306
#
#
#######################################

cleanup_mysqldump() {
    local dir="$1"
    local site="$2"

    # 按修改时间从新到旧排序
    mapfile -t dump_files < <(
        ls -1t "${dir}/${site}_"*.sql.gz 2>/dev/null || true
    )
    if ((${#dump_files[@]} > KEEP_BRF)); then
	local f="${dump_files[@]:KEEP_BRF}"
        rm -f -- "$f"
        log "_ _ _ removed: $(basename "$f")"
    fi
}

mysqldump_do() {
    local outdir="$1"
    local site="$2"
    local dbs="$3"

    tarfile=${outdir}/${site}_${NOW}.sql.gz

    log "backup mysql databases: $site [${dbs} ]"
    mysqldump --defaults-extra-file=/etc/my.cnf.d/backup_user.cnf --opt \
        --max_allowed_packet=8M --net_buffer_length=128K \
        --databases $dbs \
        | gzip -9 > $tarfile   || log "WARN: mysqldump failed for $dbs"
    chmod 664 "$tarfile"
    size_bytes=$(stat -c '%s' "$tarfile")
    size_human=$(du -h "$tarfile" | awk '{print $1}')
    log "$(basename "$tarfile")  file size: ${size_bytes} bytes (${size_human})"

    scp -4 -i "$SSH_KEY" "$tarfile" \
        "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"  || log "WARN: scp failed for $tarfile"

    cleanup_mysqldump "$outdir" "$site"
}


log ""
# log "----------------------------------------"

site="site1_db"
dbs="blog"
outdir=$LOCAL_SAVE
mysqldump_do "$outdir" "$site" "$dbs"


site="site8_db"
dbs="mydb1 mydb2 mydb3"
outdir=$LOCAL_SAVE
mysqldump_do "$outdir" "$site" "$dbs"




#######################################
# retrieving remote disk usage
#######################################

log ""
# log "----------------------------------------"
log "retrieve remote disk usage"
remote_df
log ""


send_report_mail

log "All done."



