Improve exclude rule handling and docs

This commit is contained in:
Codex
2026-03-16 18:33:06 +08:00
parent d4b69f10f9
commit 39f4066d85
5 changed files with 182 additions and 21 deletions
+160 -4
View File
@@ -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)
+2
View File
@@ -8,6 +8,8 @@
# 4. 排除目录时,建议同时写:
# */dir
# */dir/*
# 5. 上面“写两行”的建议只适用于本文件。
# exclude.user.list 是程序先解析的高级规则,不需要用户自己再补一行 /*。
# ============================================================
# ============================================================
+7 -6
View File
@@ -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/*
+5
View File
@@ -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')"
+8 -11
View File
@@ -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`