File size: 11,214 Bytes
d7dd7b9
 
de9ddf2
 
d7dd7b9
de9ddf2
 
 
 
 
 
2725d1a
de9ddf2
 
 
 
 
 
 
 
 
 
 
 
 
c63c907
de9ddf2
 
 
 
d7dd7b9
de9ddf2
 
 
 
 
 
d7dd7b9
de9ddf2
 
 
 
 
 
 
 
c63c907
de9ddf2
 
 
 
 
d7dd7b9
 
 
 
 
 
de9ddf2
 
d7dd7b9
de9ddf2
d7dd7b9
 
 
de9ddf2
 
 
c63c907
 
de9ddf2
 
 
c63c907
de9ddf2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d7dd7b9
 
de9ddf2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d7dd7b9
de9ddf2
2725d1a
de9ddf2
 
 
 
 
 
d7dd7b9
c63c907
de9ddf2
 
 
 
 
2725d1a
de9ddf2
 
 
 
 
 
 
 
 
 
 
 
2725d1a
de9ddf2
 
 
 
 
2725d1a
 
de9ddf2
 
 
 
 
d7dd7b9
de9ddf2
 
 
 
 
d7dd7b9
de9ddf2
 
 
 
 
 
 
 
 
 
 
 
 
 
2725d1a
c63c907
de9ddf2
 
 
c63c907
de9ddf2
 
d7dd7b9
de9ddf2
 
 
d7dd7b9
de9ddf2
 
 
d7dd7b9
 
de9ddf2
d7dd7b9
de9ddf2
d7dd7b9
 
 
de9ddf2
 
 
c63c907
 
de9ddf2
 
 
 
 
 
 
 
c63c907
de9ddf2
 
 
 
 
 
 
c63c907
de9ddf2
 
c63c907
de9ddf2
c63c907
de9ddf2
 
c63c907
de9ddf2
d7dd7b9
de9ddf2
 
 
 
d7dd7b9
de9ddf2
 
 
d7dd7b9
 
 
de9ddf2
 
 
 
 
c63c907
de9ddf2
 
 
 
 
 
2725d1a
de9ddf2
 
 
c63c907
de9ddf2
 
 
 
 
 
 
 
c63c907
de9ddf2
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
#!/bin/bash

# --- 配置检查 ---
# 检查 WebDAV 环境变量是否设置
if [[ -z "$WEBDAV_URL" ]] || [[ -z "$WEBDAV_USERNAME" ]] || [[ -z "$WEBDAV_PASSWORD" ]]; then
    echo "错误:缺少必要的 WebDAV 环境变量 (WEBDAV_URL, WEBDAV_USERNAME, WEBDAV_PASSWORD)。"
    echo "备份功能将不可用。"
    # 根据你的需求,如果缺少环境变量就完全退出,或者只跳过备份相关功能
    # 这里选择退出,因为脚本的主要目的是备份
    exit 1
fi

# --- 路径和常量定义 ---
# 定义要备份的本地目录 (修改点 1:明确指定路径)
LOCAL_DATA_DIR="/home/user/data"
# 定义备份文件名前缀 (修改点 2:更改前缀)
BACKUP_PREFIX="data_backup_"
# 定义 WebDAV 上的可选子目录路径
WEBDAV_BACKUP_PATH=${WEBDAV_BACKUP_PATH:-""} # 默认为空,即 WebDAV 根目录
# 构造完整的 WebDAV URL
FULL_WEBDAV_URL="${WEBDAV_URL}"
if [ -n "$WEBDAV_BACKUP_PATH" ]; then
    # 确保 URL 拼接时斜杠正确
    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
# 定义 Python 虚拟环境路径 (如果需要,请修改)
VENV_PATH="$HOME/venv/bin/activate"
# 定义同步间隔(秒),默认为 600 秒 (10 分钟)
SYNC_INTERVAL=${SYNC_INTERVAL:-600}
# 定义保留的备份数量
KEEP_BACKUPS=5

# --- 虚拟环境激活 ---
# 检查并激活 Python 虚拟环境 (如果 Python 脚本需要)
if [ -f "$VENV_PATH" ]; then
    echo "激活 Python 虚拟环境: $VENV_PATH"
    source "$VENV_PATH"
else
    echo "警告:未找到 Python 虚拟环境 $VENV_PATH。如果 Python 脚本需要特定库,可能会失败。"
fi

# --- 函数定义 ---

# 函数:从 WebDAV 恢复最新备份
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)
"
    # 检查 Python 脚本的退出码
    if [ $? -ne 0 ]; then
        echo "恢复备份过程中发生错误,脚本终止。"
        exit 1
    fi
}

# 函数:同步数据到 WebDAV
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)
        # 使用新的前缀生成备份文件名 (修改点 2)
        backup_file="${BACKUP_PREFIX}${timestamp}.tar.gz"
        local_temp_backup_path="/tmp/${backup_file}"
        remote_backup_path="${FULL_WEBDAV_URL}/${backup_file}" # 确保 URL 没有双斜杠

        echo "正在压缩目录: $LOCAL_DATA_DIR$local_temp_backup_path"
        # 使用 tar 压缩指定目录的内容 (修改点 1: 使用正确的源目录)
        # -C "$LOCAL_DATA_DIR" 表示先切换到该目录,然后压缩 '.' (当前目录的所有内容)
        # --warning=no-file-changed 忽略文件在压缩过程中改变的警告
        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 上传文件到 WebDAV
        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"

            # 清理 WebDAV 上的旧备份文件
            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
}

# --- 主逻辑 ---

# 1. 首次启动时尝试恢复最新备份 (可选,根据你的需求决定是否需要)
echo "检查并恢复最新备份(如果存在)..."
restore_backup

# 2. 等待一段时间后启动应用程序 (如果需要)
# 如果你不需要这个脚本来启动其他应用,可以注释掉下面两行
# echo "等待 30 秒后启动应用程序..."
# sleep 30
# echo "启动应用程序..."
# ./app server & # 你的应用程序启动命令

# 3. 启动后台同步进程
echo "启动后台数据同步进程..."
sync_data &

# 让主脚本保持运行(如果需要,例如在 Docker 容器中)
# 如果上面的 `./app server &` 是前台运行的,或者你用其他方式保持脚本运行,就不需要这个
# 如果 `sync_data` 是唯一需要长时间运行的,它已经在后台了,主脚本可以退出
# 但如果这是容器的主进程,你可能需要一个前台等待
# 例如: wait $! # 等待最后一个后台进程 (sync_data)
# 或者,如果启动了 app server: wait $(pgrep -f app server) # 等待 app server 进程
# 或者简单地无限循环:
# tail -f /dev/null # 保持脚本在前台运行

echo "脚本初始化完成,同步进程已在后台运行。"
# 根据你的运行环境决定脚本最后如何结束或保持运行