最近买的独服自己加了个2t的硬盘,装的pve开始玩,因为是小厂,最近在考虑备份的问题,如果是大厂就没这么多担忧了。
首先是网站/数据库/docker,这个可以用面板来备份,小鸡鸡以及其他手搓项目的备份的话就不能用面板了。
看了下别人的方法,都不是很适合我,于是,开始琢磨手搓一个备份脚本。
搭配面板的计划任务,可以使用脚本备份多个目录的文件打包后传到alist挂载的网盘内,也可以是存储桶。
主要是相对网上大多数的方法而言,更加的简单灵活,但网站备份方面依赖面板的备份,当然也可以直接指定项目运行的目录,免去使用面板生成备份文件。
开始配置
首先在alist内挂载存储,可以是本地,也可以是网盘&存储桶,我现在用的是夸克网盘,因为虚拟机的快照过大,存储桶显然不是性价比最高的选择。
然后在alist设置→对象存储→生成id以及密钥→添加一个存储桶,设定名称以及选择你备份文件存储的网盘&存储桶(挂载网盘教程参考官方文档)。
然后复制粘贴代码保存到你想存储的目录下即可,使用简单,无需复杂配置。
脚本模块示例:
/root/backup/
├── backup.sh # 主脚本
├── config.conf # 配置文件
├── logs/ # 日志目录
├── temp/ # 临时目录
└── snar/ # snar文件目录
├── dir1.snar # 对应目录1的snar文件
├── dir2.snar # 对应目录2的snar文件
└── ...
备份流程:
- 读取配置文件
- 创建临时目录和日志目录
- 检查每个备份目录的变化
- 创建增量备份包
- 上传到WebDAV
- 清理临时文件
- 记录日志
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] 备份任务完成