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

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

首先是网站/数据库/docker,这个可以用面板来备份,小鸡鸡以及其他手搓项目的备份的话就不能用面板了。
看了下别人的方法,都不是很适合我,于是,开始琢磨手搓一个备份脚本。

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

开始配置

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

然后复制粘贴代码保存到你想存储的目录下即可,使用简单,无需复杂配置。 :v:

脚本模块示例:

/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操作
通过 HTTP PUT 方法将文件传输
支持断点续传(未验证)
验证上传完整性

验证可行性

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

配置文件

# 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] 备份任务完成

上传18G文件日志

[2024-12-22 07:28:10] [INFO] 开始备份任务...
[2024-12-22 07:28:10] [BACKUP] 开始备份目录: /var/lib/vz/dump/
[2024-12-22 07:28:10] [INFO] 创建完整备份: /var/lib/vz/dump/
[2024-12-22 07:42:07] [BACKUP] 备份完成: /var/lib/vz/dump/
└── 备份文件大小: 18.47GB
[2024-12-22 07:42:07] [INFO] 备份耗时: 837 秒
[2024-12-22 07:42:07] [UPLOAD] 开始上传: /root/tut_backup/temp/backup_20241222_072810_dump.tar.gz 到 /夸克/tut_backup/20241222_072810/dump.tar.gz
├── 文件大小: 18.47GB
├── 开始时间: 2024-12-22 07:42:07
[2024-12-22 08:50:44] [UPLOAD_COMPLETE] 上传完成: /root/tut_backup/temp/backup_20241222_072810_dump.tar.gz
├── 结束时间: 2024-12-22 08:50:44
├── 耗时: 4117 秒
├── 平均速度: 4.59MB/s
└── 传输完成
[2024-12-22 08:50:44] [INFO] 开始清理临时文件...
[2024-12-22 08:50:45] [INFO] 备份任务完成
8 个赞

感谢佬分享,很有用

不知道什么时候会用到哦~
先收藏吃灰吧!

增量备份到webdav好像有一个现成的工具rclone,不过还是要说大佬牛逼。

1 个赞

感谢大佬