我已经找到了几个类似的 cron 作业脚本,但没有一个完全符合我的需要,而且我对 Linux 脚本的了解不够,无法在遇到这种可能会带来灾难性后果的作业时尝试修改代码。
本质上,我有可以录制的 IP 摄像机,/home/ben/ftp/surveillance/
但我需要确保磁盘上始终有足够的空间来录制视频。
有人可以指导我如何设置 cron 作业来:
检查/dev/sbd/
容量是否已达到 90%。如果已达到,则删除最旧的文件(以及子文件夹中的文件)/home/ben/ftp/surveillance/
并重复此操作,直到/dev/sbd/
容量低于 80% 每 10 分钟重复一次。
答案1
为人们编写此类脚本总是让我感到紧张,因为一旦出现任何问题,就会发生以下三件事之一:
- 我会因为一个可能是新手级别的打字错误而自责
- 我将会收到死亡威胁,因为有人盲目复制/粘贴而没有:
- 努力理解剧本
- 测试脚本
- 拥有合理的备份
- 上述所有的
因此,为了降低这三种风险,这里为您提供了一个入门套件:
#!/bin/sh
DIR=/home/ben/ftp/surveillance
ACT=90
df -k $DIR | grep -vE '^Filesystem' | awk '{ print $5 " " $1 }' | while read output;
do
echo $output
usep=$(echo $output | awk '{ print $1}' | cut -d'%' -f1 )
partition=$(echo $output | awk '{ print $2 }' )
if [ $usep -ge $ACT ]; then
echo "Running out of space \"$partition ($usep%)\" on $(hostname) as on $(date)"
oldfile=$(ls -dltr $DIR/*.gz|awk '{ print $9 }' | head -1)
echo "Let's Delete \"$oldfile\" ..."
fi
done
注意事项:
此脚本不会删除任何内容
DIR
是要使用的目录ACT
是采取行动所需的最低百分比只有一个文件(最旧的文件)被选中进行“删除”
您将需要将其替换
*.gz
为您的监控视频的实际文件类型。
请勿*.*
单独使用*
!如果包含的分区的
DIR
容量大于ACT
,您将看到如下消息:97% /dev/sda2 Running out of space "/dev/sda2 (97%)" on ubuntu-vm as on Wed Jan 12 07:52:20 UTC 2022 Let's Delete "/home/ben/ftp/surveillance/1999-12-31-video.gz" ...
再次,这个脚本将不会删除任何内容。
如果您对输出感到满意,那么您可以继续修改脚本以根据需要删除/移动/存档
经常测试。认真测试。记住:编写rm
脚本后,无法撤消。
答案2
我会使用 Python 来完成这样的任务。这可能需要比纯 Bash 解决方案更多的代码,但是:
- 在我看来,它更容易测试,只需使用
pytest
或unitest
模块化 - 非 Linux 用户也可以读懂(除了
get_device
Linux 特有的功能……) - 更容易上手(再次强调 IMO)
- 如果您想发送一些电子邮件怎么办? 触发新操作? 使用 Python 等编程语言可以轻松丰富脚本。
从 Python 3.3 开始,shutil
模块带有一个名为disk_usage
. 它可用于根据给定的目录获取磁盘使用情况。
小问题是我不知道如何轻松获取磁盘的名称,即/dev/sdb
,即使可以获取其磁盘使用情况(例如,/dev/sdb
在我的情况下,使用安装在 上的任何目录)。我为此目的编写了一个函数。$HOME
get_device
#!/usr/bin/env python3
import argparse
from os.path import getmtime
from shutil import disk_usage, rmtree
from sys import exit
from pathlib import Path
from typing import Iterator, Tuple
def get_device(path: Path) -> str:
"""Find the mount for a given directory. This is needed only for logging purpose."""
# Read /etc/mtab to learn about mount points
mtab_entries = Path("/etc/mtab").read_text().splitlines()
# Create a dict of mount points and devices
mount_points = dict([list(reversed(line.split(" ")[:2])) for line in mtab_entries])
# Find the mount point of given path
while path.resolve(True).as_posix() not in mount_points:
path = path.parent
# Return device associated with mount point
return mount_points[path.as_posix()]
def get_directory_and_device(path: str) -> Tuple[str, Path]:
"""Exit the process if directory does not exist."""
fs_path = Path(path)
# Path must exist
if not fs_path.exists():
print(f"ERROR: No such directory: {path}")
exit(1)
# And path must be a valid directory
if not fs_path.is_dir():
print(f"Path must be a directory and not a file: {path}")
exit(1)
# Get the device
device = get_device(fs_path)
return device, fs_path
def get_disk_usage(path: Path) -> float:
# shutil.disk_usage support Path like objects so no need to cast to string
usage = disk_usage(path)
# Get disk usage in percentage
return usage.used / usage.total * 100
def remove_file_or_directory(path: Path) -> None:
"""Remove given path, which can be a directory or a file."""
# Remove files
if path.is_file():
path.unlink()
# Recursively delete directory trees
if path.is_dir():
rmtree(path)
def find_oldest_files(
path: Path, pattern: str = "*", threshold: int = 80
) -> Iterator[Path]:
"""Iterate on the files or directories present in a directory which match given pattern."""
# List the files in the directory received as argument and sort them by age
files = sorted(path.glob(pattern), key=getmtime)
# Yield file paths until usage is lower than threshold
for file in files:
usage = get_disk_usage(path)
if usage < threshold:
break
yield file
def check_and_clean(
path: str,
threshold: int = 80,
remove: bool = False,
) -> None:
"""Main function"""
device, fspath = get_directory_and_device(path)
# shutil.disk_usage support Path like objects so no need to cast to string
usage = disk_usage(path)
# Take action if needed
if usage > threshold:
print(
f"Disk usage is greather than threshold: {usage:.2f}% > {threshold}% ({device})"
)
# Iterate over files to remove
for file in find_oldest_files(fspath, "*", threshold):
print(f"Removing file {file}")
if remove:
remove_file_or_directory(file)
def main() -> None:
parser = argparse.ArgumentParser(
description="Purge old files when disk usage is above limit."
)
parser.add_argument(
"path", help="Directory path where files should be purged", type=str
)
parser.add_argument(
"--threshold",
"-t",
metavar="T",
help="Usage threshold in percentage",
type=int,
default=80,
)
parser.add_argument(
"--remove",
"--rm",
help="Files are not removed unless --removed or --rm option is specified",
action="store_true",
default=False,
)
args = parser.parse_args()
check_and_clean(
args.path,
threshold=args.threshold,
remove=args.remove,
)
if __name__ == "__main__":
main()
如果您需要使用 CRON 协调多项任务,那么可能值得将一些 Python 代码放在一起作为库,并在许多任务中重复使用此代码。
编辑:我终于在脚本中添加了 CLI 部分,我想我会自己使用它
答案3
检查
/dev/sbd/
容量是否已达到 90%。如果已达到,则删除最旧的文件(以及子文件夹中的文件)/home/ben/ftp/surveillance/
并重复此操作,直到/dev/sbd/
容量低于 80% 每 10 分钟重复一次。
下面的脚本将执行此操作(前提是您将其添加到crontab
以 10 分钟为间隔运行的脚本中)。请特别确定这是您真正想要执行的操作,因为这很容易抹去全部/home/ben/ftp/surveillance/
如果您的磁盘已填满该目录之外的某个地方,则请将文件放入其中。
#!/bin/sh
directory='/home/ben/ftp/surveillance'
max_usage=90
goal_usage=80
[ -d "$directory" ] || exit 1
[ "$max_usage" -gt "$goal_usage" ] || exit 1
[ "$( df --output=pcent $directory | \
grep -Ewo '[0-9]+' )" -ge "$max_usage" ] || exit 0
dev_used="$( df -B 1K --output=used $directory | \
grep -Ewo '[0-9]+' )"
goal_usage="$( printf "%.0f" \
$( echo ".01 * $goal_usage * \
$( df -B 1K --output=size $directory | \
grep -Ewo '[0-9]+' )" | bc ) )"
echo "$( find $directory -type f -printf '%Ts,%k,\047%p\047\n' )" | \
sort -k1 | \
awk -F, -v goal="$(($dev_used-$goal_usage))" '\
(sum+$2)>goal{printf "%s ",$3; exit} \
(sum+$2)<=goal{printf "%s ",$3}; {sum+=$2}' | \
xargs rm
此脚本的工作原理:
shebang 后面的前三行是每个参数的变量:
directory
是包含要从中删除旧文件的文件和子目录的父目录的完整路径(即)/home/ben/ftp/surveillance
。除非路径包含空格,否则无需将此值括在引号中。max_usage
是触发旧文件删除操作的磁盘容量百分比(即90
百分比)。goal_usage
是删除旧文件后希望达到的磁盘容量百分比(即80
百分比)。
max_usage
请注意,和的值goal_usage
必须是整数。
[ -d "$directory" ] || exit 1
- 检查是否
directory
存在,否则脚本结束并以状态 1 退出。
[ "$max_usage" -gt "$goal_usage" ] || exit 1
- 检查是否
max_usage
大于goal_usage
,否则脚本结束并以状态 1 退出。
[ "$( df --output=pcent $directory | \
grep -Ewo '[0-9]+' )" -ge "$max_usage" ] || exit 0
- 获取当前使用的磁盘容量百分比,并检查它是否达到或超过 设置的阈值
max_usage
。如果不满足,则不需要进一步处理,因此脚本结束并以状态 0 退出。
dev_used="$( df -B 1K --output=used $directory | \
grep -Ewo '[0-9]+' )"
- 获取当前使用的磁盘容量千字节数。
goal_usage="$( printf "%.0f" \
$( echo ".01 * $goal_usage * \
$( df -B 1K --output=size $directory | \
grep -Ewo '[0-9]+' )" | bc ) )"
- 将变量转换
goal_usage
为千字节(我们以后会需要这个值)。
find $directory -type f -printf '%Ts,%k,\047%p\047\n'
- 找到
directory
(及其所有子目录中)的所有文件并列出这些文件,每行一个,格式为timestamp, size in kilobytes, 'full/path/to/file'
。请注意‘文件完整路径’用单引号引起来,这样文件或目录名称中的空格就不会在以后引起问题。
sort -k1
echo
按时间戳对先前的文件列表进行排序(最早的在前)。
awk -F, -v goal="$(($dev_used-$goal_usage))"
awk
创建一个内部变量goal
,该变量等于dev_used
和之间的差值goal_usage
- 这是为了将磁盘容量百分比降至goal_usage
脚本开始时设置的值而必须删除的文件的总千字节数。
(sum+$2)>goal{printf "%s ",$3; exit} \
(sum+$2)<=goal{printf "%s ",$3}; {sum+=$2}'
awk
(继续)开始处理列表,通过保持字段 2 值的运行总和(大小(以千字节为单位))并打印字段 3 的值(‘文件完整路径’) 转换为以空格分隔的字符串,直到字段 2 中的千字节数总和大于goal
,此时awk
停止处理额外的行。
xargs rm
- 'full/path/to/file' 值的字符串通过
awk
管道传输到该命令,该命令使用该字符串作为参数xargs
运行。这将删除这些文件。rm
答案4
现有的答案都存在一些问题。
有一个答案是用 Python 编写的,但你不应该用 Python 编写 shell 脚本(或者,如果你能帮忙的话,什么都不要)。python 脚本执行类似 shell 的操作更笨拙(并且使用更多代码)。有一种自然语言可用于这些操作,那就是 shell。对于某些事情,你应该避免使用 shell 代码,对于某些事情,你应该不是。
得票最高的答案使用了 shell,但它做的一些事情介于风格问题和错误行为之间。以下是一些:
- 不应使用大写变量名,以避免与任何当前或潜在的未来 POSIX 标准化变量名冲突。小写变量名明确保留供此类应用程序使用。
echo
不应该使用,因为它是不可移植;printf
是规范的、可移植的替代方案。- 的输出
ls
是仅供人眼观看。它发出的文本不能保证与任何文件的真实名称相同。此外,如果可能存在旨在造成危害的精心设计的文件名(这里的危害可能是“删除系统上的任何文件”),那么它就不安全。如果您有 GNU,ls
您至少可以使用--quoting-style=shell-escape
来减轻这种情况。最好还是避免ls
- 带有空格的路径无法正确处理,因此无法起作用。
用 shell 编写的另一个答案要好得多,但仍然存在一些问题。
- 可以使用
/bin/sh
,但由于使用了不可移植的 shell 命令和开关,因此仍然不可移植。 - 包含空格的路径仍然会中断。
- 无法引用所有扩展,这也容易受到精心设计的文件名攻击。
这些问题较小,可以很容易地解决,但我仍然不喜欢这种方法。
这是我写的替代方案,bash
应该是安全可靠的。
#!/bin/bash
dir=${1:-/home/ben/ftp/surveillance/}
threshhold=90
! [[ -d $dir ]] && {
printf '%s: not a directory\n' "$dir" 1>&2
exit 1
}
use_percent=$(df --output=pcent "$dir" | tail -n 1)
if (( ${use_percent%'%'} < threshhold )); then
exit
fi
recent-files () {
[[ -z $1 ]] || [[ $1 == *[!0-9-]* ]] && return 1
local number="$1" i=0 rev=(-r)
shift
if (( number < 0 )); then
((number*=-1))
rev=()
fi
find "${@:-.}" -maxdepth 1 -type f -printf '%T@/%p\0' | \
sort -zn "${rev[@]}" | cut -z -d/ -f2- | \
while IFS= read -rd '' file; do
printf -- '%s\0' "$file"
if (( ++i == number )); then
exit
fi
done
}
oldest=$(recent-files -1 "$dir")
size="$(stat -c %s "$oldest" | numfmt --to=iec-i)"
{
printf 'Running out of space for %s\n' "$dir"
printf 'Removing "%s"; %s freed.\n' "$oldest" "$size"
rm -f -- "$oldest"
} | logger -s -t surveillance-monitor -p local0.warning
我使用 bash 特有的功能并假设使用 GNU coreutils。sh
理论上可以实现完全兼容的版本,只使用可移植的开关,但需要做更多的工作。其他解决方案实际上已经与非可移植的df
开关等绑定在一起,因此几乎没有什么价值损失。我在这里明确假设使用 GNU/Linux,而其他人都隐含地假设了它。如果你有 GNU/Linux,你可以写更好的脚本,当你能够变得更好时你就应该变得更好。
输出消息通过 syslog 记录,因此如果您不想,则无需从 crond 检查邮件。事实上,我建议使用这个 cron 作业:
*/10 * * * * /path/to/this/script 1>/dev/null 2>&1
这假定支持 vixie cron 语法,并将每 10 分钟运行一次脚本并丢弃所有输出。您可以检查 syslog 以获取结果。(关于 cron 作业 PATH 的常见警告适用,但默认系统 PATH 应该包括我引用的每个实用程序)。
壳牌检测报告该脚本没有问题,这是在运行在互联网上找到的代码之前应该始终检查的内容。
但它是如何工作的呢?
我将解释那些我认为对于普通观察者来说最不明显的部分,但不是全部。
检查[ -z $1 ]] || [[ $1 == *[!0-9-]* ]]
确保第一个参数是正数或负数。该函数需要这样的第一个参数,所以这只是一点安全措施。
该find ... -printf '%T@/%p\0'
部分产生一个以 NULL 分隔的输出,因为 NULL 是仅有的安全分隔符在涉及文件名时使用。通过这种方式生成的每条记录都有一个以 结尾的前缀,其中/
包含文件的 mtime,描述为 Unix 纪元(以秒为单位),后跟.
秒的小数部分。例如1679796113.043092340
我/
随意选择了分隔符;任何其他分隔符都[0-9\.]
可以。您也可以\0
在这里使用,而且可以说应该使用。
使用sort -z
指示sort
期望输入记录具有 NULL 分隔符并生成具有相同分隔符的输出记录。使用 的cut -z
作用相同cut
;其他cut
开关会删除时间部分——一旦输出按 mtime 排序,我们就不再关心实际值。
while 循环用于将发出的文件数限制为指定的值(在本例中为-1
)。正值表示最近的 N 个文件,负值表示最近的 N 个文件至少最近的文件——这就是这种情况所需要的。
该numfmt
调用将生成的原始字节数转换stat
为人类可读的版本(使用IEC 二进制表示法)这是不必要的,但是很友好。
这logger
命令可能没有得到应有的广泛认可,但是它使得从脚本写入系统日志变得简单。
我认为其他一切都已经足够不言自明了。