当磁盘容量超过 90% 时,Ubuntu 自动删除目录中最旧的文件,重复此操作,直到容量低于 80%

当磁盘容量超过 90% 时,Ubuntu 自动删除目录中最旧的文件,重复此操作,直到容量低于 80%

我已经找到了几个类似的 cron 作业脚本,但没有一个完全符合我的需要,而且我对 Linux 脚本的了解不够,无法在遇到这种可能会带来灾难性后果的作业时尝试修改代码。

本质上,我有可以录制的 IP 摄像机,/home/ben/ftp/surveillance/但我需要确保磁盘上始终有足够的空间来录制视频。

有人可以指导我如何设置 cron 作业来:

检查/dev/sbd/容量是否已达到 90%。如果已达到,则删除最旧的文件(以及子文件夹中的文件)/home/ben/ftp/surveillance/并重复此操作,直到/dev/sbd/容量低于 80% 每 10 分钟重复一次。

答案1

为人们编写此类脚本总是让我感到紧张,因为一旦出现任何问题,就会发生以下三件事之一:

  1. 我会因为一个可能是新手级别的打字错误而自责
  2. 我将会收到死亡威胁,因为有人盲目复制/粘贴而没有:
    • 努力理解剧本
    • 测试脚本
    • 拥有合理的备份
  3. 上述所有的

因此,为了降低这三种风险,这里为您提供了一个入门套件:

#!/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

注意事项:

  1. 此脚本不会删除任何内容

  2. DIR是要使用的目录

  3. ACT是采取行动所需的最低百分比

  4. 只有一个文件(最旧的文件)被选中进行“删除”

  5. 您将需要将其替换*.gz为您的监控视频的实际文件类型。
    请勿*.*单独使用*

  6. 如果包含的分区的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" ...
    

    再次,这个脚本将不会删除任何内容。

  7. 如果您对输出感到满意,那么您可以继续修改脚本以根据需要删除/移动/存档

经常测试。认真测试。记住:编写rm脚本后,无法撤消。

答案2

我会使用 Python 来完成这样的任务。这可能需要比纯 Bash 解决方案更多的代码,但是:

  • 在我看来,它更容易测试,只需使用pytestunitest模块化
  • 非 Linux 用户也可以读懂(除了get_deviceLinux 特有的功能……)
  • 更容易上手(再次强调 IMO)
  • 如果您想发送一些电子邮件怎么办? 触发新操作? 使用 Python 等编程语言可以轻松丰富脚本。

从 Python 3.3 开始,shutil模块带有一个名为disk_usage. 它可用于根据给定的目录获取磁盘使用情况。

小问题是我不知道如何轻松获取磁盘的名称,即/dev/sdb,即使可以获取其磁盘使用情况(例如,/dev/sdb在我的情况下,使用安装在 上的任何目录)。我为此目的编写了一个函数。$HOMEget_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命令可能没有得到应有的广泛认可,但是它使得从脚本写入系统日志变得简单。

我认为其他一切都已经足够不言自明了。

相关内容