mirror of
https://github.com/wolfydw/backup_OBS.git
synced 2026-05-08 06:37:02 +08:00
Improve exclude rule handling and docs
This commit is contained in:
@@ -26,6 +26,13 @@ log() {
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/lib/telegram.sh"
|
||||
|
||||
RUNTIME_EXCLUDE_FILE=""
|
||||
|
||||
cleanup_tmp() {
|
||||
[[ -n "${RUNTIME_EXCLUDE_FILE:-}" ]] && rm -f "$RUNTIME_EXCLUDE_FILE"
|
||||
}
|
||||
trap cleanup_tmp EXIT
|
||||
|
||||
on_error() {
|
||||
local exit_code=$?
|
||||
local line_no=${BASH_LINENO[0]:-}
|
||||
@@ -209,6 +216,156 @@ build_tar_file_list() {
|
||||
printf '%s\n' "${args[@]}"
|
||||
}
|
||||
|
||||
archive_path_for_abs() {
|
||||
# 将真实文件系统绝对路径映射为 tar 归档内路径。
|
||||
local abs_input="$1"
|
||||
local root abs_root root_base rel
|
||||
|
||||
for root in "${BACKUP_PATHS[@]}"; do
|
||||
abs_root="$(readlink -f "$root")"
|
||||
root_base="$(basename "$abs_root")"
|
||||
|
||||
if [[ "$abs_input" == "$abs_root" ]]; then
|
||||
printf '%s\n' "$root_base"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$abs_input" == "$abs_root"/* ]]; then
|
||||
rel="${abs_input#"$abs_root"/}"
|
||||
printf '%s/%s\n' "$root_base" "$rel"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
append_exclude_path() {
|
||||
local archive_path="$1"
|
||||
local path_type="$2"
|
||||
|
||||
printf '%s\n' "$archive_path" >> "$RUNTIME_EXCLUDE_FILE"
|
||||
if [[ "$path_type" == "dir" ]]; then
|
||||
printf '%s/*\n' "$archive_path" >> "$RUNTIME_EXCLUDE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
match_user_rule() {
|
||||
# 用户规则支持两类写法:
|
||||
# 1. 绝对路径,如 /root/data/dify
|
||||
# 2. 通配规则,如 */.halo
|
||||
# 解析后统一转换成 tar 能识别的实际排除项。
|
||||
local rule="$1"
|
||||
local matched=0
|
||||
local root abs_root root_base entry archive_path path_type
|
||||
local rule_tail find_name has_glob
|
||||
|
||||
if [[ "$rule" == /* ]]; then
|
||||
if [[ ! -e "$rule" ]]; then
|
||||
log "WARN" "用户排除规则未命中任何现有路径:$rule"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! archive_path="$(archive_path_for_abs "$(readlink -f "$rule")")"; then
|
||||
log "WARN" "用户排除规则不在任何备份根目录内:$rule"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -d "$rule" ]]; then
|
||||
path_type="dir"
|
||||
else
|
||||
path_type="file"
|
||||
fi
|
||||
append_exclude_path "$archive_path" "$path_type"
|
||||
return 0
|
||||
fi
|
||||
|
||||
has_glob=false
|
||||
if [[ "$rule" == *'*'* || "$rule" == *'?'* || "$rule" == *'['* ]]; then
|
||||
has_glob=true
|
||||
fi
|
||||
|
||||
for root in "${BACKUP_PATHS[@]}"; do
|
||||
abs_root="$(readlink -f "$root")"
|
||||
root_base="$(basename "$abs_root")"
|
||||
|
||||
if ! $has_glob; then
|
||||
entry=""
|
||||
if [[ "$rule" == "$root_base" ]]; then
|
||||
entry="$abs_root"
|
||||
elif [[ "$rule" == "$root_base"/* ]]; then
|
||||
entry="${abs_root}/${rule#"$root_base"/}"
|
||||
fi
|
||||
|
||||
if [[ -n "$entry" && -e "$entry" ]]; then
|
||||
matched=1
|
||||
if [[ -d "$entry" ]]; then
|
||||
path_type="dir"
|
||||
else
|
||||
path_type="file"
|
||||
fi
|
||||
append_exclude_path "$rule" "$path_type"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
rule_tail="${rule##*/}"
|
||||
if [[ "$rule_tail" == *'*'* || "$rule_tail" == *'?'* || "$rule_tail" == *'['* ]]; then
|
||||
while IFS= read -r -d '' entry; do
|
||||
archive_path="$(archive_path_for_abs "$entry" || true)"
|
||||
[[ -n "$archive_path" ]] || continue
|
||||
if [[ "$archive_path" == $rule ]]; then
|
||||
matched=1
|
||||
if [[ -d "$entry" ]]; then
|
||||
path_type="dir"
|
||||
else
|
||||
path_type="file"
|
||||
fi
|
||||
append_exclude_path "$archive_path" "$path_type"
|
||||
fi
|
||||
done < <(find "$abs_root" -mindepth 1 -print0 2>/dev/null)
|
||||
else
|
||||
find_name="$rule_tail"
|
||||
while IFS= read -r -d '' entry; do
|
||||
archive_path="$(archive_path_for_abs "$entry" || true)"
|
||||
[[ -n "$archive_path" ]] || continue
|
||||
if [[ "$archive_path" == $rule ]]; then
|
||||
matched=1
|
||||
if [[ -d "$entry" ]]; then
|
||||
path_type="dir"
|
||||
else
|
||||
path_type="file"
|
||||
fi
|
||||
append_exclude_path "$archive_path" "$path_type"
|
||||
fi
|
||||
done < <(find "$abs_root" -mindepth 1 -name "$find_name" -print0 2>/dev/null)
|
||||
fi
|
||||
done
|
||||
|
||||
if (( matched == 0 )); then
|
||||
log "WARN" "用户排除规则未命中任何路径:$rule"
|
||||
fi
|
||||
}
|
||||
|
||||
build_runtime_exclude_file() {
|
||||
# 默认规则直接交给 tar。
|
||||
# 用户规则先解析成真实命中路径,再换算为归档内排除项。
|
||||
local rule
|
||||
RUNTIME_EXCLUDE_FILE="$(mktemp)"
|
||||
cat "$DEFAULT_EXCLUDE_FILE" > "$RUNTIME_EXCLUDE_FILE"
|
||||
|
||||
[[ -f "$USER_EXCLUDE_FILE" ]] || return 0
|
||||
|
||||
while IFS= read -r rule; do
|
||||
[[ -n "$rule" ]] || continue
|
||||
[[ "$rule" =~ ^[[:space:]]*# ]] && continue
|
||||
match_user_rule "$rule"
|
||||
done < "$USER_EXCLUDE_FILE"
|
||||
|
||||
awk '!seen[$0]++' "$RUNTIME_EXCLUDE_FILE" > "${RUNTIME_EXCLUDE_FILE}.dedup"
|
||||
mv "${RUNTIME_EXCLUDE_FILE}.dedup" "$RUNTIME_EXCLUDE_FILE"
|
||||
}
|
||||
|
||||
check_space() {
|
||||
local total=0
|
||||
local p sz avail need
|
||||
@@ -247,15 +404,14 @@ pack() {
|
||||
fi
|
||||
|
||||
log "INFO" "开始打包(支持默认与用户排除规则),pigz=${USE_PIGZ}"
|
||||
build_runtime_exclude_file
|
||||
if $USE_PIGZ; then
|
||||
tar --warning=no-file-changed \
|
||||
--exclude-from="$DEFAULT_EXCLUDE_FILE" \
|
||||
--exclude-from="$USER_EXCLUDE_FILE" \
|
||||
--exclude-from="$RUNTIME_EXCLUDE_FILE" \
|
||||
-c "${tar_args[@]}" | pigz > "$ARCHIVE_PATH"
|
||||
else
|
||||
tar --warning=no-file-changed \
|
||||
--exclude-from="$DEFAULT_EXCLUDE_FILE" \
|
||||
--exclude-from="$USER_EXCLUDE_FILE" \
|
||||
--exclude-from="$RUNTIME_EXCLUDE_FILE" \
|
||||
-czf "$ARCHIVE_PATH" "${tar_args[@]}"
|
||||
fi
|
||||
end=$(date +%s)
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
# 4. 排除目录时,建议同时写:
|
||||
# */dir
|
||||
# */dir/*
|
||||
# 5. 上面“写两行”的建议只适用于本文件。
|
||||
# exclude.user.list 是程序先解析的高级规则,不需要用户自己再补一行 /*。
|
||||
# ============================================================
|
||||
|
||||
# ============================================================
|
||||
|
||||
@@ -12,20 +12,21 @@
|
||||
#
|
||||
# 规则示例:
|
||||
# - 排除单个文件
|
||||
# */project/src/main.js
|
||||
# /root/myapp/config.json
|
||||
#
|
||||
# - 排除一个目录
|
||||
# */project/cache
|
||||
# */project/cache/*
|
||||
# /root/data/dify
|
||||
# */.halo
|
||||
#
|
||||
# - 排除某个固定路径
|
||||
# - 排除某个固定路径(推荐直接写绝对路径)
|
||||
# /root/data/dify
|
||||
#
|
||||
# - 排除任意位置的隐藏目录
|
||||
# */.halo
|
||||
# */.halo/*
|
||||
# 说明:
|
||||
# 程序会在实际备份目录中匹配这条规则,并自动判断命中的是文件还是目录。
|
||||
# 如果规则当前没有命中任何实际路径,脚本会记录 WARN,但不会直接退出失败。
|
||||
# ============================================================
|
||||
|
||||
/root/data/dify
|
||||
*/.halo
|
||||
*/.halo/*
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# 统一的 Telegram 通知模块:
|
||||
# - 负责消息模板拼装
|
||||
# - 负责把动态内容放进代码块,降低 MarkdownV2 出错概率
|
||||
# - 供 backup.sh 和 clean_backups.sh 共用
|
||||
|
||||
tg_code_block() {
|
||||
local text="${1-}"
|
||||
text="$(printf '%s' "$text" | sed -e 's/\\/\\\\/g' -e 's/`/\\`/g')"
|
||||
|
||||
@@ -69,26 +69,23 @@ EOF
|
||||
- `exclude.user.list`:用户自定义排除规则,不提交 Git
|
||||
- `exclude.user.list.example`:用户规则示例文件,可以提交 Git
|
||||
|
||||
排除规则由 `tar --exclude-from` 读取,匹配的是归档内的相对路径,不是磁盘上的绝对路径。
|
||||
默认规则仍然走 `tar --exclude-from`。
|
||||
用户自定义规则现在会在实际备份目录里先做一次解析,再自动换算成归档路径,所以不需要你手动猜 tar 看到的相对路径。
|
||||
如果某条用户规则当前没有命中任何实际路径,脚本会记录 `WARN`,但不会因此中止备份。
|
||||
|
||||
排除单个文件:
|
||||
推荐写法 1:绝对路径
|
||||
|
||||
```text
|
||||
project/src/main.js
|
||||
/root/data/dify
|
||||
```
|
||||
|
||||
或者:
|
||||
推荐写法 2:通配模式
|
||||
|
||||
```text
|
||||
*/project/src/main.js
|
||||
*/.halo
|
||||
```
|
||||
|
||||
排除一个目录,建议写两行:
|
||||
|
||||
```text
|
||||
*/project/cache
|
||||
*/project/cache/*
|
||||
```
|
||||
对于 `*/xxxx` 这类规则,程序会在实际备份目录中先匹配命中的对象,再自动判断命中的是文件还是目录,因此用户不需要额外写一行 `/*` 来区分目录内容。
|
||||
|
||||
默认规则放在 `exclude.list`,业务自定义规则建议只写到 `exclude.user.list`。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user