|
|
#!/bin/bash |
|
|
|
|
|
|
|
|
|
|
|
if [[ -z "$WEBDAV_URL" ]] || [[ -z "$WEBDAV_USERNAME" ]] || [[ -z "$WEBDAV_PASSWORD" ]]; then |
|
|
echo "错误:缺少必要的 WebDAV 环境变量 (WEBDAV_URL, WEBDAV_USERNAME, WEBDAV_PASSWORD)。" |
|
|
echo "备份功能将不可用。" |
|
|
|
|
|
|
|
|
exit 1 |
|
|
fi |
|
|
|
|
|
|
|
|
|
|
|
LOCAL_DATA_DIR="/data" |
|
|
|
|
|
BACKUP_PREFIX="data_backup_" |
|
|
|
|
|
WEBDAV_BACKUP_PATH=${WEBDAV_BACKUP_PATH:-""} |
|
|
|
|
|
FULL_WEBDAV_URL="${WEBDAV_URL}" |
|
|
if [ -n "$WEBDAV_BACKUP_PATH" ]; then |
|
|
|
|
|
if [[ "${WEBDAV_URL}" != */ ]]; then |
|
|
WEBDAV_URL="${WEBDAV_URL}/" |
|
|
fi |
|
|
if [[ "${WEBDAV_BACKUP_PATH}" == /* ]]; then |
|
|
WEBDAV_BACKUP_PATH="${WEBDAV_BACKUP_PATH:1}" |
|
|
fi |
|
|
FULL_WEBDAV_URL="${WEBDAV_URL}${WEBDAV_BACKUP_PATH}" |
|
|
fi |
|
|
|
|
|
VENV_PATH="$HOME/venv/bin/activate" |
|
|
|
|
|
SYNC_INTERVAL=${SYNC_INTERVAL:-600} |
|
|
|
|
|
KEEP_BACKUPS=5 |
|
|
|
|
|
|
|
|
|
|
|
if [ -f "$VENV_PATH" ]; then |
|
|
echo "激活 Python 虚拟环境: $VENV_PATH" |
|
|
source "$VENV_PATH" |
|
|
else |
|
|
echo "警告:未找到 Python 虚拟环境 $VENV_PATH。如果 Python 脚本需要特定库,可能会失败。" |
|
|
fi |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
restore_backup() { |
|
|
echo "开始从 WebDAV 下载最新备份..." |
|
|
python3 -c " |
|
|
import sys |
|
|
import os |
|
|
import tarfile |
|
|
import requests |
|
|
import shutil |
|
|
from webdav3.client import Client |
|
|
options = { |
|
|
'webdav_hostname': '$FULL_WEBDAV_URL', |
|
|
'webdav_login': '$WEBDAV_USERNAME', |
|
|
'webdav_password': '$WEBDAV_PASSWORD' |
|
|
} |
|
|
local_data_dir = '$LOCAL_DATA_DIR' # 使用变量 |
|
|
backup_prefix = '$BACKUP_PREFIX' # 使用变量 |
|
|
try: |
|
|
client = Client(options) |
|
|
# 过滤文件,使用正确的前缀 (修改点 2) |
|
|
backups = [f for f in client.list() if f.endswith('.tar.gz') and f.startswith(backup_prefix)] |
|
|
if not backups: |
|
|
print(f'在 {options['webdav_hostname']} 没有找到前缀为 {backup_prefix} 的备份文件。') |
|
|
# 如果没有备份,确保目标目录存在但为空可能是期望行为,或者直接退出 |
|
|
os.makedirs(local_data_dir, exist_ok=True) |
|
|
print(f'确保目录 {local_data_dir} 存在。') |
|
|
sys.exit(0) # 正常退出,因为没有备份可恢复 |
|
|
latest_backup = sorted(backups)[-1] |
|
|
print(f'找到最新备份文件:{latest_backup}') |
|
|
remote_file_url = f'{options['webdav_hostname']}/{latest_backup}' # 假设 URL 构造正确 |
|
|
local_temp_file = f'/tmp/{latest_backup}' |
|
|
print(f'正在从 {remote_file_url} 下载...') |
|
|
with requests.get(remote_file_url, auth=(options['webdav_login'], options['webdav_password']), stream=True) as r: |
|
|
r.raise_for_status() # 检查 HTTP 错误 |
|
|
with open(local_temp_file, 'wb') as f: |
|
|
for chunk in r.iter_content(chunk_size=8192): |
|
|
f.write(chunk) |
|
|
print(f'成功下载备份文件到 {local_temp_file}') |
|
|
if os.path.exists(local_temp_file): |
|
|
print(f'准备恢复到目录: {local_data_dir}') |
|
|
# 如果目录已存在,先清空它 (或者删除重建,取决于需求) |
|
|
if os.path.exists(local_data_dir): |
|
|
print(f'目录 {local_data_dir} 已存在,正在清空...') |
|
|
# shutil.rmtree(local_data_dir) # 删除整个目录树 |
|
|
# 或者清空目录内容 |
|
|
for item in os.listdir(local_data_dir): |
|
|
item_path = os.path.join(local_data_dir, item) |
|
|
if os.path.isfile(item_path) or os.path.islink(item_path): |
|
|
os.unlink(item_path) |
|
|
elif os.path.isdir(item_path): |
|
|
shutil.rmtree(item_path) |
|
|
else: |
|
|
os.makedirs(local_data_dir, exist_ok=True) |
|
|
print(f'正在解压 {local_temp_file} 到 {local_data_dir}...') |
|
|
# 解压备份文件 (修改点 1: 确保解压到正确的目录) |
|
|
with tarfile.open(local_temp_file, 'r:gz') as tar: |
|
|
tar.extractall(path=local_data_dir) |
|
|
print(f'成功从 {latest_backup} 恢复备份到 {local_data_dir}') |
|
|
os.remove(local_temp_file) # 清理临时文件 |
|
|
else: |
|
|
print(f'错误:下载的备份文件 {local_temp_file} 未找到。') |
|
|
except requests.exceptions.RequestException as e: |
|
|
print(f'下载备份时发生网络错误: {e}') |
|
|
sys.exit(1) |
|
|
except Exception as e: |
|
|
print(f'恢复备份时发生错误: {e}') |
|
|
sys.exit(1) |
|
|
" |
|
|
|
|
|
if [ $? -ne 0 ]; then |
|
|
echo "恢复备份过程中发生错误,脚本终止。" |
|
|
exit 1 |
|
|
fi |
|
|
} |
|
|
|
|
|
|
|
|
sync_data() { |
|
|
while true; do |
|
|
echo "-----------------------------------------------------" |
|
|
echo "开始同步过程于: $(date)" |
|
|
|
|
|
|
|
|
if [ ! -d "$LOCAL_DATA_DIR" ]; then |
|
|
echo "警告:本地数据目录 $LOCAL_DATA_DIR 不存在。跳过此次备份。" |
|
|
sleep $SYNC_INTERVAL |
|
|
continue |
|
|
fi |
|
|
|
|
|
|
|
|
if [ -z "$(ls -A $LOCAL_DATA_DIR)" ]; then |
|
|
echo "信息:本地数据目录 $LOCAL_DATA_DIR 为空。跳过此次备份。" |
|
|
sleep $SYNC_INTERVAL |
|
|
continue |
|
|
fi |
|
|
|
|
|
timestamp=$(date +%Y%m%d_%H%M%S) |
|
|
|
|
|
backup_file="${BACKUP_PREFIX}${timestamp}.tar.gz" |
|
|
local_temp_backup_path="/tmp/${backup_file}" |
|
|
remote_backup_path="${FULL_WEBDAV_URL}/${backup_file}" |
|
|
|
|
|
echo "正在压缩目录: $LOCAL_DATA_DIR 到 $local_temp_backup_path" |
|
|
|
|
|
|
|
|
|
|
|
tar --warning=no-file-changed -czf "$local_temp_backup_path" -C "$LOCAL_DATA_DIR" . |
|
|
|
|
|
if [ $? -ne 0 ]; then |
|
|
echo "错误:压缩目录 $LOCAL_DATA_DIR 失败。" |
|
|
rm -f "$local_temp_backup_path" |
|
|
sleep $SYNC_INTERVAL |
|
|
continue |
|
|
fi |
|
|
|
|
|
|
|
|
if [ ! -s "$local_temp_backup_path" ]; then |
|
|
echo "错误:压缩后的文件 $local_temp_backup_path 大小为 0 或不存在。跳过上传。" |
|
|
rm -f "$local_temp_backup_path" |
|
|
sleep $SYNC_INTERVAL |
|
|
continue |
|
|
fi |
|
|
|
|
|
|
|
|
echo "正在上传备份文件: $local_temp_backup_path 到 $remote_backup_path" |
|
|
|
|
|
curl --fail -u "$WEBDAV_USERNAME:$WEBDAV_PASSWORD" -T "$local_temp_backup_path" "$remote_backup_path" |
|
|
|
|
|
if [ $? -eq 0 ]; then |
|
|
echo "成功上传 ${backup_file} 到 WebDAV" |
|
|
|
|
|
|
|
|
echo "清理本地临时文件: $local_temp_backup_path" |
|
|
rm -f "$local_temp_backup_path" |
|
|
|
|
|
|
|
|
echo "开始清理 WebDAV 上的旧备份..." |
|
|
python3 -c " |
|
|
import sys |
|
|
from webdav3.client import Client |
|
|
options = { |
|
|
'webdav_hostname': '$FULL_WEBDAV_URL', |
|
|
'webdav_login': '$WEBDAV_USERNAME', |
|
|
'webdav_password': '$WEBDAV_PASSWORD' |
|
|
} |
|
|
backup_prefix = '$BACKUP_PREFIX' # 使用变量 |
|
|
keep_backups = $KEEP_BACKUPS # 使用变量 |
|
|
try: |
|
|
client = Client(options) |
|
|
# 过滤文件,使用正确的前缀 (修改点 2) |
|
|
backups = [f for f in client.list() if f.endswith('.tar.gz') and f.startswith(backup_prefix)] |
|
|
backups.sort() # 按名称排序(时间戳) |
|
|
if len(backups) > keep_backups: |
|
|
to_delete_count = len(backups) - keep_backups |
|
|
print(f'找到 {len(backups)} 个备份,超过了保留数量 {keep_backups},将删除最早的 {to_delete_count} 个。') |
|
|
for file_to_delete in backups[:to_delete_count]: |
|
|
try: |
|
|
# 确保路径正确,对于根目录下的文件,不需要加斜杠 |
|
|
delete_path = file_to_delete |
|
|
print(f'正在删除旧备份: {delete_path}') |
|
|
client.clean(delete_path) # clean 方法通常用于删除文件或目录 |
|
|
print(f'成功删除 {delete_path}') |
|
|
except Exception as delete_err: |
|
|
print(f'删除文件 {delete_path} 时出错: {delete_err}') |
|
|
else: |
|
|
print(f'找到 {len(backups)} 个备份,未达到保留上限 {keep_backups},无需清理。') |
|
|
except Exception as e: |
|
|
print(f'清理旧备份时发生错误: {e}') |
|
|
" |
|
|
if [ $? -ne 0 ]; then |
|
|
echo "警告:清理 WebDAV 旧备份时发生错误。" |
|
|
fi |
|
|
|
|
|
else |
|
|
echo "错误:上传 ${backup_file} 到 WebDAV 失败。返回码: $?" |
|
|
|
|
|
echo "清理本地临时文件: $local_temp_backup_path" |
|
|
rm -f "$local_temp_backup_path" |
|
|
fi |
|
|
|
|
|
echo "下次同步将在 ${SYNC_INTERVAL} 秒后进行..." |
|
|
sleep $SYNC_INTERVAL |
|
|
done |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
echo "检查并恢复最新备份(如果存在)..." |
|
|
restore_backup |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
echo "启动后台数据同步进程..." |
|
|
sync_data & |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
echo "脚本初始化完成,同步进程已在后台运行。" |
|
|
|