#!/usr/bin/env bash
# ============================================================================
# 文件名: increment_backup.sh
# 功  能: rsync 增量快照备份脚本（硬链接 + 自动保留策略）
# 依  赖: rsync, flock
# 版  本: v2.1
# 作  者: 树辉 <qiushuhui@oschina.cn>
# ============================================================================

set -o errexit
set -o nounset
set -o pipefail

# ------------------------------------------------------------------
# 用户配置区（请根据实际环境修改）
# ------------------------------------------------------------------

# rsync 服务端信息
readonly RSYNC_AUTH_USER='gitee'
readonly RSYNC_AUTH_PASSWORD='790a002393c3e022d50a917a3d26fafedf842'
readonly RSYNC_AUTH_FILE='/etc/rsyncd.auth'   # 优先使用认证文件（更安全）
readonly RSYNC_SERVER='192.168.80.50'
readonly RSYNC_PORT="873"
readonly RSYNC_MODULE_NAME="gitaly"

# 目录结构
readonly BACKUP_BASE_DIR='/data/backup'
readonly BACKUP_DIR="${BACKUP_BASE_DIR}/${RSYNC_MODULE_NAME}"
readonly SNAPSHOT_DIR="${BACKUP_DIR}/snapshots"
readonly LOG_DIR="${BACKUP_DIR}/logs"

# 保留策略（按天清理）
readonly KEEP_DAYS=15                          # 快照保留天数

# 锁文件
readonly LOCKFILE="/tmp/rsync_backup_${RSYNC_MODULE_NAME}.lock"

# 超时
readonly RSYNC_TIMEOUT=3600                    # rsync 超时时间（秒）

# ------------------------------------------------------------------
# 以下内容一般无需修改
# ------------------------------------------------------------------

readonly SCRIPT_NAME=$(basename "$0")
readonly DATE_STAMP=$(date +"%Y-%m-%d")        # 2026-06-06
readonly LOG_FILE="${LOG_DIR}/backup_$(date +%Y%m%d_%H%M%S).log"

# ------------------------------------------------------------------
# 日志函数（终端带颜色输出，日志文件纯文本）
# ------------------------------------------------------------------
# 颜色定义
readonly C_CYAN='\033[0;36m'
readonly C_GREEN='\033[0;32m'
readonly C_YELLOW='\033[0;33m'
readonly C_RED='\033[0;31m'
readonly C_BOLD='\033[1m'
readonly C_RESET='\033[0m'

log_info() {
    local msg="[INFO]  $(date '+%Y-%m-%d %H:%M:%S') $*"
    printf "${C_CYAN}%s${C_RESET}\n" "$msg"
    echo "$msg" >> "$LOG_FILE"
}

log_warn() {
    local msg="[WARN]  $(date '+%Y-%m-%d %H:%M:%S') $*"
    printf "${C_YELLOW}%s${C_RESET}\n" "$msg"
    echo "$msg" >> "$LOG_FILE"
}

log_error() {
    local msg="[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*"
    printf "${C_RED}%s${C_RESET}\n" "$msg" >&2
    echo "$msg" >> "$LOG_FILE"
}

log_separator() {
    printf "${C_BOLD}${C_CYAN}%s${C_RESET}\n" "================================================"
    echo "================================================" >> "$LOG_FILE"
}

log_header() {
    local msg="$*"
    printf "${C_BOLD}${C_GREEN}%s${C_RESET}\n" "$msg"
    echo "$msg" >> "$LOG_FILE"
}

# ------------------------------------------------------------------
# 前置校验
# ------------------------------------------------------------------
preflight_check() {
    # 检查 root 权限
    if [[ "$(id -u)" -ne "0" ]]; then
        echo "错误: 请使用 root 用户运行此脚本！"
        exit 1
    fi

    # 检查 rsync 是否可用
    if ! command -v rsync &> /dev/null; then
        echo "错误: 未找到 rsync 命令，请先安装！"
        exit 1
    fi

    # 创建必要目录
    mkdir -p "$SNAPSHOT_DIR" "$LOG_DIR"

    # 配置认证信息
    setup_auth

    # 获取互斥锁
    exec 200>"${LOCKFILE}"
    if ! flock -n 200; then
        log_error "已有备份进程在运行（锁文件: ${LOCKFILE}），退出"
        exit 1
    fi
    trap 'flock -u 200 2>/dev/null' EXIT
    log_info "获取互斥锁: ${LOCKFILE}"
}

# ------------------------------------------------------------------
# 认证配置（认证文件优先，环境变量回退）
# ------------------------------------------------------------------
setup_auth() {
    if [[ -f "$RSYNC_AUTH_FILE" ]]; then
        # 认证文件已存在，使用它（更安全，不会通过 ps 泄露密码）
        chmod 600 "$RSYNC_AUTH_FILE"
        # 确保环境变量不被设置，避免冲突
        unset RSYNC_PASSWORD
        log_info "使用认证文件: ${RSYNC_AUTH_FILE}"
    else
        # 创建认证文件并写入密码
        echo "${RSYNC_AUTH_PASSWORD}" > "$RSYNC_AUTH_FILE"
        chmod 600 "$RSYNC_AUTH_FILE"
        # 同时设置环境变量兼容（实际 rsync 读文件时不需环境变量）
        export RSYNC_PASSWORD="${RSYNC_AUTH_PASSWORD}"
        log_info "自动创建认证文件: ${RSYNC_AUTH_FILE}"
    fi
}

# ------------------------------------------------------------------
# 执行 rsync 同步
# 参数: 额外 rsync 选项（字符串形式）
# ------------------------------------------------------------------
run_rsync() {
    local extra_opts="$1"
    local snapshot_path="$2"
    local rsync_exit_code=0

    # 构建 rsync 命令
    local rsync_cmd=(
        rsync -avh
        --port="${RSYNC_PORT}"
        --stats
        --quiet
        --delete
        --timeout="${RSYNC_TIMEOUT}"
        --partial            # 保留部分传输的文件，断点续传
        --password-file="${RSYNC_AUTH_FILE}"
    )

    # 追加额外选项
    if [[ -n "$extra_opts" ]]; then
        # shellcheck disable=SC2206
        rsync_cmd+=($extra_opts)
    fi

    rsync_cmd+=(
        "rsync://${RSYNC_AUTH_USER}@${RSYNC_SERVER}/${RSYNC_MODULE_NAME}"
        "${snapshot_path}"
    )

    log_info "执行 rsync 命令（目标: ${snapshot_path}）"

    # 执行 rsync，捕获退出码
    # 进度输出（--info=progress2）通过 tee 同时显示到终端和日志文件
    set +o errexit
    "${rsync_cmd[@]}" 2>&1 | tee -a "$LOG_FILE"
    rsync_exit_code=${PIPESTATUS[0]}
    set -o errexit

    if [[ $rsync_exit_code -eq 0 ]]; then
        log_info "rsync 同步成功"
        return 0
    elif [[ $rsync_exit_code -eq 24 ]]; then
        # 退出码 24 = 部分文件传输因源文件消失而跳过（正常现象）
        log_warn "rsync 退出码 ${rsync_exit_code}（源文件在传输中消失，不影响整体备份）"
        return 0
    else
        log_error "rsync 失败（退出码: ${rsync_exit_code}）"
        return 1
    fi
}

# ------------------------------------------------------------------
# 清理过期快照
# ------------------------------------------------------------------
cleanup_snapshots() {
    log_info "开始清理过期快照（保留 ${KEEP_DAYS} 天的快照）..."

    local removed=0

    while IFS= read -r -d '' dir; do
        local dirname
        dirname=$(basename "$dir")

        # 跳过非日期目录（YYYY-MM-DD 格式）
        [[ "$dirname" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || continue

        # 计算天数差
        local dir_epoch
        dir_epoch=$(date -d "${dirname}" +%s 2>/dev/null) || continue
        local now_epoch
        now_epoch=$(date +%s)
        local age_days=$(( (now_epoch - dir_epoch) / 86400 ))

        if [[ $age_days -gt $KEEP_DAYS ]]; then
            log_info "  删除过期快照: ${dirname}（已 ${age_days} 天）"
            rm -rf "$dir"
            ((removed++))
        fi
    done < <(find "$SNAPSHOT_DIR" -mindepth 1 -maxdepth 1 -type d -print0)

    if [[ $removed -eq 0 ]]; then
        log_info "没有需要清理的快照"
    else
        log_info "共清理 ${removed} 个过期快照"
    fi

    # 清理过期日志文件
    find "$LOG_DIR" -name 'backup_*.log' -mtime +"${KEEP_DAYS}" -delete 2>/dev/null || true
}

# ------------------------------------------------------------------
# 更新 latest 软链接
# ------------------------------------------------------------------
update_latest_symlink() {
    local target="$1"
    ln -snf "$target" "${BACKUP_DIR}/latest"
    log_info "更新 latest 软链接 -> ${target}"
}

# ------------------------------------------------------------------
# 主流程
# ------------------------------------------------------------------
main() {
    log_separator
    log_header "备份任务启动"
    log_info "  日期:      $(date '+%Y-%m-%d %A')"
    log_info "  模块:      ${RSYNC_MODULE_NAME}"
    log_info "  服务器:    ${RSYNC_SERVER}:${RSYNC_PORT}"
    log_info "  快照目录:  ${SNAPSHOT_DIR}"
    log_info "  保留策略:  最近 ${KEEP_DAYS} 天"
    log_separator

    local snapshot_path="${SNAPSHOT_DIR}/${DATE_STAMP}"
    local start_epoch start_time
    start_epoch=$(date +%s)
    start_time=$(date '+%Y-%m-%d %H:%M:%S')

    # ============================================================
    # 判断备份类型：有基准目录 → 增量，无基准 → 全量
    # ============================================================
    local backup_type link_target link_target_resolved

    link_target="${BACKUP_DIR}/latest"
    link_target_resolved=""
    if [[ -d "${link_target}" ]]; then
        link_target_resolved=$(readlink -f "${link_target}" 2>/dev/null || echo "")
        if [[ -n "${link_target_resolved}" && -d "${link_target_resolved}" ]]; then
            backup_type="增量备份"
        else
            link_target_resolved=""
            backup_type="全量备份（latest 指向无效）"
        fi
    else
        backup_type="全量备份（首次运行，无基准目录）"
    fi

    log_info "${backup_type} — $(date '+%A')"

    if [[ -n "${link_target_resolved}" ]]; then
        run_rsync "--link-dest=${link_target_resolved}" "$snapshot_path"
    else
        run_rsync "" "$snapshot_path"
    fi

    update_latest_symlink "$snapshot_path"

    # 清理过期快照
    cleanup_snapshots

    # ============================================================
    # 统计汇总
    # ============================================================
    local end_epoch elapsed_sec elapsed_human end_time
    end_epoch=$(date +%s)
    end_time=$(date '+%Y-%m-%d %H:%M:%S')
    elapsed_sec=$(( end_epoch - start_epoch ))

    # 格式化耗时
    local h=$(( elapsed_sec / 3600 ))
    local m=$(( (elapsed_sec % 3600) / 60 ))
    local s=$(( elapsed_sec % 60 ))
    if [[ "$h" -gt 0 ]]; then
        elapsed_human="${h}h${m}m${s}s"
    elif [[ "$m" -gt 0 ]]; then
        elapsed_human="${m}m${s}s"
    else
        elapsed_human="${s}s"
    fi

    # 统计本次快照
    if [[ -d "$snapshot_path" ]]; then
        local usage file_count dir_count
        usage=$(du -sh "$snapshot_path" 2>/dev/null | awk '{print $1}')
        file_count=$(find "$snapshot_path" -type f | wc -l)
        dir_count=$(find "$snapshot_path" -type d | wc -l)
        log_info "本次快照 ${DATE_STAMP} 磁盘占用: ${usage}"
        log_info "文件数量:  $(printf "%'d" "${file_count}")"
        log_info "目录数量:  $(printf "%'d" "${dir_count}")"
    fi

    # 汇总信息
    log_separator
    log_header "备份完成汇总:"
    log_info   "启动时间:  ${start_time}"
    log_info   "结束时间:  ${end_time}"
    log_info   "备份耗时:  ${elapsed_human}"
    log_info   "模块:      ${RSYNC_MODULE_NAME}"
    log_info   "快照:      ${DATE_STAMP}"
    log_info   "保留策略:  最近 [ ${KEEP_DAYS} ] 天"

    local total_snapshots
    total_snapshots=$(find "$SNAPSHOT_DIR" -mindepth 1 -maxdepth 1 -type d | wc -l)
    log_info   "当前快照数: [ ${total_snapshots} ]"
    log_separator
    log_header "备份任务完成 ✅"
}

# ------------------------------------------------------------------
# 执行入口
# ------------------------------------------------------------------
preflight_check

if main; then
    exit 0
else
    log_error "脚本主流程异常退出，请查看日志: ${LOG_FILE}"
    exit 1
fi
