我需要的
我在创建一个脚本(bash?)时遇到问题,该脚本将在 cron 中使用,每晚运行,无人值守,该脚本会删除所有备份目录,除了所有第一个月的备份和最新的 14 个备份,即使较旧。如果 bash 不行,那么可以使用 shell 和 POSIX,因为我需要它是可移植的。
脚本必须安全、优雅并且我被困在哪里:认识到自 5 月以来没有发生任何备份,并且仍然保留最近(5 月)的 14 个备份,因为这些是最新的,即使脚本在 11 月运行也是如此。在所有情况下,脚本必须保留名称中日期部分为 01 的所有备份 (-YYYYMMDD-)。
我拥有的
- 我有包含备份的 DIRS
- 备份日期位于 DIR 名称中
- 脚本必须读取内容
/path/to/backups/example.com/
并决定删除其中的哪个 DIRS - DIRS 不为空。它们包含当天的备份。
/path/to/backups/example.com/example.com-20210101-backup/ // Keep (first of month)
/path/to/backups/example.com/example.com-20210201-backup/ // Keep (first of month)
/path/to/backups/example.com/example.com-20210301-backup/ // Keep (first of month)
/path/to/backups/example.com/example.com-20210401-backup/ // Keep (first of month)
/path/to/backups/example.com/example.com-20210501-backup/ // Keep (first of month)
/path/to/backups/example.com/example.com-20210502-backup/ // <-- Script to remove
/path/to/backups/example.com/example.com-20210503-backup/ // <-- Script to remove
/path/to/backups/example.com/example.com-20210504-backup/ // <-- Script to remove
/path/to/backups/example.com/example.com-20210505-backup/ // <-- Script to remove
/path/to/backups/example.com/example.com-20210506-backup/ // <-- Script to remove
/path/to/backups/example.com/example.com-20210507-backup/ // <-- Script to remove
/path/to/backups/example.com/example.com-20210508-backup/ // <-- Script to remove
/path/to/backups/example.com/example.com-20210509-backup/ // <-- Script to remove
/path/to/backups/example.com/example.com-20210510-backup/ // <-- Script to remove
/path/to/backups/example.com/example.com-20210511-backup/ // <-- Script to remove
/path/to/backups/example.com/example.com-20210512-backup/ // <-- Script to remove
/path/to/backups/example.com/example.com-20210513-backup/ // <-- Script to remove
/path/to/backups/example.com/example.com-20210514-backup/ // <-- Script to remove
/path/to/backups/example.com/example.com-20210515-backup/ // Keep (Most recent 14 days even if old)
/path/to/backups/example.com/example.com-20210516-backup/ // Keep (Most recent 14 days even if old)
/path/to/backups/example.com/example.com-20210517-backup/ // Keep (Most recent 14 days even if old)
/path/to/backups/example.com/example.com-20210518-backup/ // Keep (Most recent 14 days even if old)
/path/to/backups/example.com/example.com-20210519-backup/ // Keep (Most recent 14 days even if old)
/path/to/backups/example.com/example.com-20210520-backup/ // Keep (Most recent 14 days even if old)
/path/to/backups/example.com/example.com-20210521-backup/ // Keep (Most recent 14 days even if old)
/path/to/backups/example.com/example.com-20210522-backup/ // Keep (Most recent 14 days even if old)
/path/to/backups/example.com/example.com-20210523-backup/ // Keep (Most recent 14 days even if old)
/path/to/backups/example.com/example.com-20210524-backup/ // Keep (Most recent 14 days even if old)
/path/to/backups/example.com/example.com-20210525-backup/ // Keep (Most recent 14 days even if old)
/path/to/backups/example.com/example.com-20210526-backup/ // Keep (Most recent 14 days even if old)
/path/to/backups/example.com/example.com-20210527-backup/ // Keep (Most recent 14 days even if old)
/path/to/backups/example.com/example.com-20210528-backup/ // Keep (Most recent 14 days even if old)
为什么要为此编写脚本
因为,有一天备份可能会恢复并有新的 DIRS 需要在无人值守的情况下删除。
我发现了什么
我找到的所有内容要么删除除最近 14 个之外的所有内容,要么只保留第一个月,而不是两者都保留。
例如,按照:https://unix.stackexchange.com/a/379041
find /path/to/backups/example.com/ -type d -mtime +14 -exec rm -rf {} +
只会显示自脚本运行之日起最近的 14 个脚本,如果在 11 月运行,则不会显示或删除任何内容。
我的麻烦
无法创建一个安全脚本来保留所有第一个月的备份和最新的 14 个备份,即使是旧的。
感谢任何为我指明正确方向的帮助,
- 谢谢你!
答案1
假设:
- 您的备份目录(至少在最顶层)是合理构建的,没有空格/换行符/正则表达式或 shell glob 元字符等。
- 你有一个路径中的目录集合
base_path
- 每个目录都以前缀开头
base_prefx
- 每个目录以后缀结尾
base_suffx
- 一旦去掉路径、前缀和后缀,每个目录名称都是一个日期
YYYYMMDD
- 不符合这些标准的目录将被忽略
有了这些给定的信息,我们就可以相应地规划我们的策略。
当前任务的关键是根据YYYYMMDD
目录名称的部分删除零个或多个目录。为了确定要删除的特定目录(如果有),我们:
DD
排除日期部分为或预期01
字段中出现任何非数字字符的所有目录YYYYMMDD
- 其余目录中,排除
N
最近的日期 - 所有剩余的目录(如果有)都将被删除
你已经选择N=14
。
#!/usr/bin/env bash
retain=14
base_path='./path/to/backups/example.com/'
base_prefx='example.com-'
base_suffx='-backup'
find "$base_path" -maxdepth 1 -mindepth 1 \
-type d \
-name "${base_prefx}????????${base_suffx}" |
while IFS= read dir
do
base="$(basename "$dir" "$base_suffx")"
printf '%s\n' "${base#$base_prefx}"
done |
grep -Ev '([^[:digit:]]|01$)' |
sort -r |
tail +$(($retain+1)) |
while IFS= read base
do
printf 'rm -rf "%q%q%q%q"\n' \
"$base_path" "$base_prefx" "$base" "$base_suffx"
done
该find
命令在base_path
目录中查找与我们假设的目录结构模板匹配的子目录名称,并且这些子目录名称正好位于该base_path
目录的下一层。
find
的输出被馈送到 while 循环,该循环读取输入的每一行,剥离base_path
,base_prefx
和base_suffx
并将base
目录名称的部分(表面上是日期)写入stdout
。
然后stdout
将其传递给grep
删除包含非数字字符的所有条目或者结束于01
.删除以 结尾的条目01
非常重要,这样可以无限期保留当月第一天的备份。
grep
然后将 的输出sort
输入下降以便最新的条目(不包括任何??????01
条目)位于输出的顶部,较新的条目位于输出的顶部。
现在我们已经排除了所有??????01
备份目录日期,并按降序对日期进行排序,其中最近的日期在前,唯一剩下的任务是跳过第一个N
条目,然后删除所有条目N+1
及更高的条目。
代码使用变量retain
来表示N
。 tail
读取sort
ed 输出并开始输出从 line 开始的行retain+1
,并且该stdout
流被传递到while
循环。
该循环将每一行读取为变量base
,并重新构造一个rm -rf
命令,该命令引用base_path
后跟 ,base_prefx
后跟base
本身,后跟base_suffx
.然后将该命令写入stdout
.
请注意,由于该rm
命令仅写入stdout
,因此该脚本不会删除任何内容。在对其进行操作之前,应检查输出的准确性。如果命令显示正确,则可以通过管道传送输出sh
并rm
执行命令。一旦您对该脚本进行了满意的测试,printf
就可以修改该行以实际调用正确的rm -rf
命令,以便可以通过cron
.
让我们创建一些目录来测试:
mkdir -p path/to/backups/example.com/example.com-20210101-backup
mkdir -p path/to/backups/example.com/example.com-20210201-backup
mkdir -p path/to/backups/example.com/example.com-20210301-backup
mkdir -p path/to/backups/example.com/example.com-20210401-backup
mkdir -p path/to/backups/example.com/example.com-20210501-backup
mkdir -p path/to/backups/example.com/example.com-20210502-backup
mkdir -p path/to/backups/example.com/example.com-20210503-backup
mkdir -p path/to/backups/example.com/example.com-20210504-backup
mkdir -p path/to/backups/example.com/example.com-20210505-backup
mkdir -p path/to/backups/example.com/example.com-20210506-backup
mkdir -p path/to/backups/example.com/example.com-20210507-backup
mkdir -p path/to/backups/example.com/example.com-20210508-backup
mkdir -p path/to/backups/example.com/example.com-20210509-backup
mkdir -p path/to/backups/example.com/example.com-20210510-backup
mkdir -p path/to/backups/example.com/example.com-20210511-backup
mkdir -p path/to/backups/example.com/example.com-20210512-backup
mkdir -p path/to/backups/example.com/example.com-20210513-backup
mkdir -p path/to/backups/example.com/example.com-20210514-backup
mkdir -p path/to/backups/example.com/example.com-20210515-backup
mkdir -p path/to/backups/example.com/example.com-20210516-backup
mkdir -p path/to/backups/example.com/example.com-20210517-backup
mkdir -p path/to/backups/example.com/example.com-20210518-backup
mkdir -p path/to/backups/example.com/example.com-20210519-backup
mkdir -p path/to/backups/example.com/example.com-20210520-backup
mkdir -p path/to/backups/example.com/example.com-20210521-backup
mkdir -p path/to/backups/example.com/example.com-20210522-backup
mkdir -p path/to/backups/example.com/example.com-20210523-backup
mkdir -p path/to/backups/example.com/example.com-20210524-backup
mkdir -p path/to/backups/example.com/example.com-20210525-backup
mkdir -p path/to/backups/example.com/example.com-20210526-backup
mkdir -p path/to/backups/example.com/example.com-20210527-backup
mkdir -p path/to/backups/example.com/example.com-20210528-backup
mkdir -p path/to/backups/example.com/example.com-20210228-backup/example.com-20210101-backup
mkdir -p path/to/backups/example.com/example.com-messedup-backup/example.com-20210227-backup
mkdir -p path/to/backups/example.com/example.com-20210428-backup/example.com-20210601-backup
然后运行脚本:
$ ./test.sh
rm -rf "./path/to/backups/example.com/example.com-20210514-backup"
rm -rf "./path/to/backups/example.com/example.com-20210513-backup"
rm -rf "./path/to/backups/example.com/example.com-20210512-backup"
rm -rf "./path/to/backups/example.com/example.com-20210511-backup"
rm -rf "./path/to/backups/example.com/example.com-20210510-backup"
rm -rf "./path/to/backups/example.com/example.com-20210509-backup"
rm -rf "./path/to/backups/example.com/example.com-20210508-backup"
rm -rf "./path/to/backups/example.com/example.com-20210507-backup"
rm -rf "./path/to/backups/example.com/example.com-20210506-backup"
rm -rf "./path/to/backups/example.com/example.com-20210505-backup"
rm -rf "./path/to/backups/example.com/example.com-20210504-backup"
rm -rf "./path/to/backups/example.com/example.com-20210503-backup"
rm -rf "./path/to/backups/example.com/example.com-20210502-backup"
rm -rf "./path/to/backups/example.com/example.com-20210428-backup"
rm -rf "./path/to/backups/example.com/example.com-20210228-backup"
看起来不错,让我们运行一下:
$ ./test.sh | sh
更新
在文件名中混合 shell 全局变量(如????????
)和正则表达式(如[0-9]{6}Z
)可能会变得不守规矩。该脚本当然可以调整为在整个过程中使用正则表达式,但会增加一点复杂性。
#!/usr/bin/env bash
retain=15
# This is a shell glob (with no wildcards); must end in slash
base_path='./path/to/backups/example.com/'
# This is an extended regex pattern:
base_regex='\./path/to/backups/example\.com/example\.com-([0-9]{8}-[0-9]{6}Z)-backup'
# This is a printf spec to printf a base_path and a date-time to a full directory name:
printf_spec='%qexample.com-%q-backup'
find -E "$base_path" -maxdepth 1 -mindepth 1 \
-type d \
-regex "${base_regex}" |
sed -Ee "s~^${base_regex}$~\1~" |
grep -Ev '^[0-9]{6}01-' |
sort -r |
tail -n +$(($retain+1)) |
while IFS= read line
do
printf "rm -rf ${printf_spec}\n" "${base_path}" "$line"
done
在顶部添加了注释,以清楚地表明哪些变量是 shell 全局变量,哪些是正则表达式,哪些是规范printf
。需要这些是因为:
base_path
需要是一个 shell glob 来告诉find
去哪里寻找。base_regex
需要是全行正则表达式,因为find ... -regex
需要一个与整行(目录名称)匹配的正则表达式。请注意,正则表达式字符.
无论出现在何处都会被转义。printf_spec
需要是一个printf
兼容的规范,它将字符串格式化YYYYMMDD-HHMMSSZ
为有效的目录名称。
现在我们可以指向find -E
并告诉它查找正好比该目录低一级的目录,其名称将与扩展的 regex$base_path
形成整行匹配(ala ) 。grep -Ex
$base_regex
请注意,正则表达式中旨在匹配的部分YYYYMMDD-HHMMSSZ
带有括号。这会创建一个“反向引用”,sed
在下一步中会很方便。我们传递find
to的整个输出sed
,并告诉它用该行中与正则表达式的括号部分匹配的部分替换输入的每一行,这是YYYYMMDD-HHMMSSZ
我们按时间顺序排序所需的部分。早期的脚本使用 bash-ism 来解析时间戳,但 bash-ism 依赖于 glob,因此为了实现基于正则表达式的解决方案,我们使用sed
.
脚本的其余部分基本相同:sed
的输出被传递到grep
以删除任何月份第一天的所有备份作业。该输出依次进入逆序sort
,tail
然后跳过$retain
列表顶部的最大值,将其后的每一行输出到将每一行传递到 的 while 循环printf
。
注意事项:
经验丰富的 U&L 用户可能会指出其他内容,但需要注意以下几点:
- 请务必转义您使用的任何预期
base_regex
与目录名称字面匹配的正则表达式字符 - 该
sed
命令使用 a~
作为搜索和替换分隔符。因此,我们必须避免在目录名称中使用波浪号。只要您不在base_regex
字符串中添加波形符,find
就应该为您消除此类目录,即使它们确实以某种方式在文件系统中创建了。 - 由于此算法将每个日期/时间组合处理为唯一的备份,因此如果昨天运行了 14 个备份作业,“保留最后 14 个备份”可能只保留昨天的备份。
答案2
谢谢@吉姆。这一直有效,直到我需要在目录名称中添加祖鲁时间,使它们成为:
./path/to/backups/example.com/example.com-20210101-040538Z-backup
笔记:祖鲁时间每天都会变化。
这打破了它。
因此,我尝试按照您的脚本添加正则表达式,尝试像这样解决该时间字符串。但以下内容不起作用,因为我确定我的正则表达式格式有错误:
#!/usr/bin/env bash
retain=15
base_path='./path/to/backups/example.com/'
base_prefx='example.com-'
base_time="-040538Z" # <--- Works but is not Regex.
# base_time="-[[:digit:]]{6}Z" # <--- Regex (I think) but not working.
base_suffx="-backup"
find "$base_path" -maxdepth 1 -mindepth 1 \
-type d \
-name "${base_prefx}????????${base_time}${base_suffx}" |
while IFS= read dir
do
base="$(basename "$dir" "$base_time$base_suffx")"
printf '%s\n' "${base#$base_prefx}"
done |
grep -Ev '([^[:digit:]]|01$)' |
sort -r |
tail -n +$(($retain+1)) |
while IFS= read base
do
printf 'success-safety-rm -rf "%q%q%q%q%q"\n' \
"$base_path" "$base_prefx" "$base" "$base_time" "$base_suffx"
done
也许我的做法是错误的?
我该如何更好地构建这个脚本?
答案3
GNU 发行版解决方案
仅供后代参考:@Jim L 上面接受的答案的更新部分是解决方案,也是其基础。
为什么要单独回答?
因为find -E
上面的“接受的答案更新”部分会导致 GNU 环境中出现错误:find: unknown predicate '-E'
。
以下是基于 GNU 的 Linux 的工作脚本:
#!/usr/bin/env bash
retain=15
# CHANGEME This is a shell glob (with no wildcards); must end in slash; example:
base_path='./path/to/backups/example.com/'
# CHANGEME This is an extended regex pattern example:
base_regex='\./path/to/backups/example\.com/example\.com-([0-9]{8}-[0-9]{6}Z)-backup'
# CHANGEME This is a printf spec to printf a base_path and a date-time to a full directory name example:
printf_spec='%qexample.com-%q-backup'
find "$base_path" -maxdepth 1 -mindepth 1 \
-type d \
-regextype posix-extended \
-regex "${base_regex}" |
sed -Ee "s~^${base_regex}$~\1~" |
grep -Ev '^[0-9]{6}01-' |
sort -r |
tail -n +$(($retain+1)) |
while IFS= read line
do
printf "REMOVEME-SAFETY-rm -rf ${printf_spec}\n" "$base_path" "$line"
done
运行:
./test.sh
结果是:
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20211105-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20211104-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20211103-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20211102-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20210303-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20210302-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20210203-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20210202-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20210110-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20210109-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20210108-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20210107-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20210106-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20210105-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20210104-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20210103-040538Z-backup
REMOVEME-SAFETY-rm -rf /path/to/backups/example.com/example.com-20210102-040538Z-backup
哪些是要从我的 example.com 测试设置中删除的备份目录。
现在,该printf "REMOVEME-SAFETY-rm...
生产线可以调整为无人值守运行,并实际删除目录。
笔记:吉姆的注意事项:上面接受的答案也代表这个版本。
再次感谢@Jim L。
答案4
其他解决方案在我的场景中无法可靠地工作,因为我想删除许多具有不同文件命名模式的不同备份。因此,我编写了以下基于文件创建(或修改)日期而不是文件名的脚本:
#!/usr/bin/env bash
dryRun=true # Set to `false` or remove this line to move from logging to deletion
rootFolder="/var/lib/psa/dumps/" # Files within this folder will be checked recursively
fileGlob="*.*" # Limit to specific file type if required
fileAgeLimit="30 days ago" # All files up to this age (date only and inclusive) will be kept
regularExpressionForDatesToBeKept='-[0-9][0-9]-01$' # This default regular expression will keep all files from the 1st of each month
checkForDeletion() {
filePath=$1
fileName=$(basename "$filePath")
fileDateTimeString=$(stat -c '%w' "$filePath") # Creation date; only available on newer file systems
if [[ "$fileDateTimeString" = "-" ]]; then
fileDateTimeString=$(stat -c '%y' "$filePath") # Use modification date instead
fi
fileDateString="$(date +"%Y-%m-%d" -d "$fileDateTimeString")"
fileAgeLimitDateString="$(date +"%Y-%m-%d" -d "$fileAgeLimit")"
if [[ "$fileDateString" < "$fileAgeLimitDateString" ]]; then
if [[ "$fileDateString" =~ $regularExpressionForDatesToBeKept ]]; then
[[ $dryRun = true ]] && echo -e "To be kept\tFile date: $fileDateString (matches '$regularExpressionForDatesToBeKept')\t$fileName"
else
if [[ $dryRun = true ]]; then
echo -e "To be DELETED\tFile date: $fileDateString\t\t\t\t$fileName"
else
rm -f "$filePath"
fi
fi
else
[[ $dryRun = true ]] && echo -e "To be kept\tFile date: $fileDateString ($fileAgeLimit or younger)\t$fileName"
fi
}
# Safely loop through all find matches
while read -r -d ''; do
checkForDeletion "$REPLY"
done < <(find "$rootFolder" -type f -name "$fileGlob" -print0)