使用shell脚本自动备份文件到WebDAV,支持增量备份。

最近买的独服自己加了个2t的硬盘,装的pve开始玩,因为是小厂,最近在考虑备份的问题,如果是大厂就没这么多担忧了。

首先是网站以及docker,这个可以用面板来备份,pve虚拟机备份的话就不能用面板了。
看了下别人的方法,都不是很适合我,于是,开始琢磨手搓一个备份脚本。

可以备份多个目录的文件打包后传到alist挂载的网盘内,也可以是存储桶。
主要是相对网上大多数的方法而言,更加的简单灵活,但网站备份方面依赖面板的备份,当然也可以直接指定项目运行的目录,免去使用面板生成备份文件。

开始配置

首先在alist内挂载存储,可以是本地,也可以是网盘&存储桶,我现在用的是夸克网盘,因为虚拟机的快照过大,存储桶显然不是性价比最高的选择。
然后在alist设置→对象存储→生成id以及密钥→添加一个存储桶,设定名称以及选择你备份文件存储的网盘&存储桶(挂载网盘教程参考官方文档)。

image

然后复制粘贴代码保存到你想存储的目录下即可。

脚本模块示例:

/root/backup/
├── backup.sh          # 主脚本
├── config.conf        # 配置文件
├── logs/              # 日志目录
├── temp/              # 临时目录
└── snar/              # snar文件目录
    ├── dir1.snar     # 对应目录1的snar文件
    ├── dir2.snar     # 对应目录2的snar文件
    └── ...

备份流程:

  1. 读取配置文件
  2. 创建临时目录和日志目录
  3. 检查每个备份目录的变化
  4. 创建增量备份包
  5. 上传到WebDAV
  6. 清理临时文件
  7. 记录日志

WebDAV上传实现:

使用 curl 命令进行WebDAV操作
支持断点续传(未验证)
验证上传完整性

验证可行性

测试了小文件的上传,打包网站什么的自然没什么问题。
然后测试了把虚拟机备份上传到webdav,18G左右的快照,跑了一个多小时上传成功了,期间没有遇到大家说的webdav的断流
大文件应该也没什么问题了,开始贴代码。

测试日志

小型备份

image
大型备份

image

配置文件

# config.conf

# WebDAV配置
WEBDAV_URL="http://127.0.0.1:5244/dav"    # 此处根据实际情况修改,后缀/dav不要删除
WEBDAV_USER="username"
WEBDAV_PASS="password"

# 备份目录配置(空格分隔多个目录)
BACKUP_DIRS="/opt/1panel/backup/"

# 远程备份目录
REMOTE_BACKUP_DIR="/夸克/backup"    # 根据你的命名自行修改

# 其他配置
COMPRESS_LEVEL=6       # 压缩级别(1-9)
INCREMENTAL_BACKUP=false    # 是否启用增量备份

脚本代码

#backup.sh

#!/bin/bash

###################
# 常量定义
###################
SCRIPT_DIR="/root/tut_backup"
CONFIG_FILE="${SCRIPT_DIR}/config.conf"
LOG_DIR="${SCRIPT_DIR}/logs"
TEMP_DIR="${SCRIPT_DIR}/temp"
SNAR_DIR="${SCRIPT_DIR}/snar"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_PREFIX="backup_${DATE}"

# 全局变量,用于进度显示
CURRENT_FILE=""
CURRENT_PROGRESS=0
CURRENT_SPEED=0

###################
# 配置文件加载
###################
# 如果配置文件不存在,提示用户
if [ ! -f "$CONFIG_FILE" ]; then
    echo "请先配置 ${CONFIG_FILE} 文件"
    exit 1
fi

# 加载配置文件
source "$CONFIG_FILE"

###################
# 依赖检查
###################
check_dependencies() {
    local missing_deps=()

    # 检查必要的命令
    for cmd in bc curl tar; do
        if ! command -v "$cmd" >/dev/null 2>&1; then
            missing_deps+=("$cmd")
        fi
    done

    # 如果有缺失的依赖,提示用户安装
    if [ ${#missing_deps[@]} -ne 0 ]; then
        echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] 缺少必要的依赖,请手动安装: ${missing_deps[*]}"
        exit 1
    fi
}

###################
# 工具函数
###################
# 获取文件大小(字节)
get_file_size() {
    local file="$1"
    if [ -f "$file" ]; then
        stat --format="%s" "$file"
    else
        echo "0"
    fi
}

# 格式化大小
format_size() {
    local size=$1
    if [ -z "$size" ] || [ "$size" -eq 0 ]; then
        echo "0B"
        return
    fi

    if [ "$size" -ge 1073741824 ]; then
        echo "$(echo "scale=2; $size/1073741824" | bc)GB"
    elif [ "$size" -ge 1048576 ]; then
        echo "$(echo "scale=2; $size/1048576" | bc)MB"
    elif [ "$size" -ge 1024 ]; then
        echo "$(echo "scale=2; $size/1024" | bc)KB"
    else
        echo "${size}B"
    fi
}

# 控制台输出函数
console_log() {
    local level=$1
    local message=$2
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] ${message}"
}

# 计算传输速度
calculate_speed() {
    local bytes=$1
    local seconds=$2

    if [ "$seconds" -eq 0 ] || [ "$bytes" -eq 0 ]; then
        echo "0"
        return
    fi

    local speed=$((bytes / seconds))
    format_size "$speed"
}

# 进度条显示函数
show_progress() {
    local current=$1
    local total=$2
    local speed=$3
    local width=50

    # 防止除以零错误
    if [ "$total" -eq 0 ] || [ -z "$total" ]; then
        return
    fi

    local percentage=$((current * 100 / total))
    local filled=$((percentage * width / 100))
    local empty=$((width - filled))

    # 确保filled和empty不为负数
    [ "$filled" -lt 0 ] && filled=0
    [ "$empty" -lt 0 ] && empty=0

    # 格式化显示
    printf "\r[$(date '+%Y-%m-%d %H:%M:%S')] [PROGRESS] "
    printf "%-30s " "$(basename "$CURRENT_FILE")"
    printf "["
    printf "%${filled}s" "" | tr ' ' '#'
    printf "%${empty}s" "" | tr ' ' '-'
    printf "] "
    printf "%3d%% " "$percentage"
    if [ -n "$speed" ]; then
        printf "%10s/s" "$speed"
    fi

    # 如果完成了,换行
    if [ "$current" -ge "$total" ]; then
        printf "\n"
    fi
}

# 日志函数
log() {
    local level=$1
    local message=$2
    local log_file="${LOG_DIR}/backup_${DATE}.log"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')

    echo "[${timestamp}] [${level}] ${message}" >> "$log_file"

    case "$level" in
        "BACKUP")
            if [ -n "$3" ]; then
                local file_size=$(get_file_size "$3")
                echo "└── 备份文件大小: $(format_size "$file_size")" >> "$log_file"
            fi
            ;;
        "UPLOAD")
            if [ -n "$3" ]; then
                local file_size=$(get_file_size "$3")
                echo "├── 文件大小: $(format_size "$file_size")" >> "$log_file"
                echo "├── 开始时间: $timestamp" >> "$log_file"
            fi
            ;;
        "UPLOAD_COMPLETE")
            local start_time=$3
            local file=$4
            local end_time=$(date +%s)
            local duration=$((end_time - start_time))
            local file_size=$(get_file_size "$file")

            if [ "$duration" -gt 0 ] && [ "$file_size" -gt 0 ]; then
                local speed=$((file_size / duration))
                local formatted_speed=$(format_size "$speed")

                echo "├── 结束时间: $timestamp" >> "$log_file"
                echo "├── 耗时: ${duration} 秒" >> "$log_file"
                echo "├── 平均速度: ${formatted_speed}/s" >> "$log_file"
                echo "└── 传输完成" >> "$log_file"
            else
                echo "├── 结束时间: $timestamp" >> "$log_file"
                echo "└── 传输完成" >> "$log_file"
            fi
            ;;
    esac
}

###################
# 错误处理函数
###################
handle_error() {
    local error_message=$1
    console_log "ERROR" "$error_message"
    log "ERROR" "$error_message"
    cleanup
    exit 1
}

###################
# 清理函数
###################
cleanup() {
    console_log "INFO" "开始清理临时文件..."
    log "INFO" "开始清理临时文件..."

    # 清理临时文件
    rm -rf "${TEMP_DIR:?}"/*

    # 如果需要清理过期的本地日志,可以设定保留天数
    # 例如,保留最近 7 天的日志
    # 如果不需要清理日志,可以注释或删除以下代码
    find "$LOG_DIR" -type f -name "*.log" -mtime +7 -delete
}

###################
# WebDAV 操作函数
###################
# WebDAV上传函数
upload_to_webdav() {
    local file="$1"
    local remote_path="$2"
    local start_time=$(date +%s)

    CURRENT_FILE="$file"
    console_log "INFO" "开始上传: ${file} 到 ${remote_path}"
    log "UPLOAD" "开始上传: ${file} 到 ${remote_path}" "$file"

    # 创建远程目录
    curl -s -X MKCOL -u "${WEBDAV_USER}:${WEBDAV_PASS}" \
         "${WEBDAV_URL}${REMOTE_BACKUP_DIR}/${DATE}" >/dev/null 2>&1

    # 使用临时文件存储进度信息
    local progress_file="${TEMP_DIR}/progress_$$"

    # 获取文件大小
    local file_size=$(get_file_size "$file")

    # 启动后台进程监控上传进度
    (
        local last_size=0
        local last_time=$start_time

        while [ -f "$progress_file" ]; do
            if [ -f "$progress_file" ]; then
                local current_time=$(date +%s)
                local transferred=$(tail -n 1 "$progress_file" 2>/dev/null | grep -o '[0-9]*' || echo "0")

                if [ -n "$transferred" ] && [ "$transferred" -gt 0 ]; then
                    local time_diff=$((current_time - last_time))
                    local size_diff=$((transferred - last_size))

                    if [ "$time_diff" -gt 0 ]; then
                        local current_speed=$(calculate_speed "$size_diff" "$time_diff")
                        show_progress "$transferred" "$file_size" "$current_speed"

                        last_size=$transferred
                        last_time=$current_time
                    fi
                fi
            fi
            sleep 1
        done
    ) &

    # 上传文件
    curl -# -T "$file" \
         -u "${WEBDAV_USER}:${WEBDAV_PASS}" \
         -H "Expect:" \
         "${WEBDAV_URL}${remote_path}" \
         2>"$progress_file" || \
         handle_error "文件上传失败: ${file}"

    # 清理进度文件
    rm -f "$progress_file"

    # 计算总体平均速度
    local end_time=$(date +%s)
    local total_time=$((end_time - start_time))
    local total_size=$(get_file_size "$file")
    local avg_speed=$(calculate_speed "$total_size" "$total_time")

    # 显示100%进度
    show_progress "$total_size" "$total_size" "$avg_speed"

    console_log "INFO" "上传完成: ${file} (平均速度: ${avg_speed}/s)"
    log "UPLOAD_COMPLETE" "上传完成: ${file}" "$start_time" "$file"
}

###################
# 备份函数
###################
create_backup() {
    local dir="$1"
    local dir_name=$(basename "$dir")
    local snar_file="${SNAR_DIR}/${dir_name}.snar"
    local backup_file="${TEMP_DIR}/${BACKUP_PREFIX}_${dir_name}.tar.gz"
    local start_time=$(date +%s)

    console_log "INFO" "开始备份目录: ${dir}"
    log "BACKUP" "开始备份目录: ${dir}"

    if [ "$INCREMENTAL_BACKUP" = "true" ] && [ -f "$snar_file" ]; then
        console_log "INFO" "创建增量备份: ${dir}"
        log "INFO" "创建增量备份: ${dir}"
        tar czf "$backup_file" -g "$snar_file" "$dir" 2>/dev/null || \
            handle_error "创建增量备份失败: ${dir}"
    else
        console_log "INFO" "创建完整备份: ${dir}"
        log "INFO" "创建完整备份: ${dir}"
        if [ "$INCREMENTAL_BACKUP" = "true" ]; then
            tar czf "$backup_file" -g "$snar_file" "$dir" 2>/dev/null || \
                handle_error "创建完整备份失败: ${dir}"
        else
            tar czf "$backup_file" "$dir" 2>/dev/null || \
                handle_error "创建完整备份失败: ${dir}"
        fi
    fi

    local end_time=$(date +%s)
    local duration=$((end_time - start_time))

    console_log "INFO" "备份完成: ${dir} (耗时: ${duration} 秒)"
    log "BACKUP" "备份完成: ${dir}" "$backup_file"
    log "INFO" "备份耗时: ${duration} 秒"

    return 0
}

###################
# 主函数
###################
main() {
    # 检查依赖
    check_dependencies

    # 创建必要的目录
    mkdir -p "$LOG_DIR" "$TEMP_DIR" "$SNAR_DIR"

    console_log "INFO" "开始备份任务..."
    log "INFO" "开始备份任务..."

    # 检查配置
    if [ -z "$BACKUP_DIRS" ]; then
        handle_error "未配置备份目录"
    fi

    # 处理每个备份目录
    for dir in $BACKUP_DIRS; do
        if [ ! -d "$dir" ]; then
            console_log "WARN" "目录不存在,跳过: ${dir}"
            log "WARN" "目录不存在,跳过: ${dir}"
            continue
        fi

        # 创建备份
        create_backup "$dir"

        # 上传到WebDAV
        local dir_name=$(basename "$dir")
        local backup_file="${TEMP_DIR}/${BACKUP_PREFIX}_${dir_name}.tar.gz"
        upload_to_webdav "$backup_file" "${REMOTE_BACKUP_DIR}/${DATE}/${dir_name}.tar.gz"
    done

    # 清理
    cleanup

    console_log "INFO" "备份任务完成"
    log "INFO" "备份任务完成"
}

# 执行主函数
main "$@"

手动执行的ssh日志示例:

# 首次运行时会检查并安装所需依赖。
root@tut:~/tut_backup# /root/backup/backup.sh
[2024-12-22 04:43:45] [WARN] 正在安装必要的依赖: bc
[2024-12-22 07:12:19] [INFO] 开始备份任务...
[2024-12-22 07:12:19] [INFO] 开始备份目录: /opt/1panel/backup/
[2024-12-22 07:12:19] [INFO] 创建增量备份: /opt/1panel/backup/
[2024-12-22 07:12:36] [INFO] 备份完成: /opt/1panel/backup/ (耗时: 17 秒)
[2024-12-22 07:12:36] [INFO] 开始上传: /root/backup/temp/backup_20241222_071219_backup.tar.gz 到 /夸克/backup/20241222_071219/backup.tar.gz
[2024-12-22 07:14:07] [PROGRESS] backup_20241222_071219_backup.tar.gz [##################################################] 100%     3.82MB/s
[2024-12-22 07:14:07] [INFO] 上传完成: /root/backup/temp/backup_20241222_071219_backup.tar.gz (平均速度: 3.82MB/s)
[2024-12-22 07:14:07] [INFO] 开始清理临时文件...
[2024-12-22 07:14:07] [INFO] 备份任务完成
消息盒子
# 您需要首次评论以获取消息 #
# 您需要首次评论以获取消息 #

只显示最新10条未读和已读信息