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

Ben*_*eno 20 cron

我发现了一些类似的 cron 作业脚本,但没有完全了解我需要它们的方式,而且我对 Linux 脚本了解不够,无法在涉及此类作业时尝试修改代码,这可能会带来灾难性的后果。

本质上,我有可以记录的网络摄像机/home/ben/ftp/surveillance/,但我需要确保磁盘上始终有足够的空间来执行此操作。

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

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

mat*_*igo 37

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

\n
    \n
  1. 我会因为这可能是 n00b 级的拼写错误而自责
  2. \n
  3. 我会收到死亡威胁,因为有人盲目地复制/粘贴而没有:\n
      \n
    • 努力理解剧本
    • \n
    • 测试脚本
    • \n
    • 有合理的备份
    • \n
    \n
  4. \n
  5. 上述所有的
  6. \n
\n

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

\n
#!/bin/sh\nDIR=/home/ben/ftp/surveillance\nACT=90\ndf -k $DIR | grep -vE '^Filesystem' | awk '{ print $5 " " $1 }' | while read output;\ndo\n  echo $output\n  usep=$(echo $output | awk '{ print $1}' | cut -d'%' -f1  )\n  partition=$(echo $output | awk '{ print $2 }' )\n  if [ $usep -ge $ACT ]; then\n    echo "Running out of space \\"$partition ($usep%)\\" on $(hostname) as on $(date)"\n    oldfile=$(ls -dltr $DIR/*.gz|awk '{ print $9 }' | head -1)\n    echo "Let's Delete \\"$oldfile\\" ..."\n  fi\ndone\n
Run Code Online (Sandbox Code Playgroud)\n

注意事项:

\n
    \n
  1. 该脚本不会删除任何内容

    \n
  2. \n
  3. DIR是要使用的目录

    \n
  4. \n
  5. ACT是采取行动所需的最低百分比

    \n
  6. \n
  7. 仅选择一个文件 \xe2\x80\x93\xc2\xa0 最旧的 \xe2\x80\x93\xc2\xa0 进行“删除”

    \n
  8. \n
  9. 您需要替换*.gz为监控视频的实际文件类型。
    请勿使用*.**单独使用!

    \n
  10. \n
  11. 如果包含的分区的DIR容量大于ACT,您将看到如下消息:

    \n
    97% /dev/sda2\nRunning out of space "/dev/sda2 (97%)" on ubuntu-vm as on Wed Jan 12 07:52:20 UTC 2022\nLet's Delete "/home/ben/ftp/surveillance/1999-12-31-video.gz" ...\n
    Run Code Online (Sandbox Code Playgroud)\n

    同样,该脚本不会删除任何内容。

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

    \n
  14. \n
\n

经常测试。好好测试一下。请记住:放入rm脚本时,无法撤消。

\n

  • @NickMatteo 如果相应的代码工作正常并且可读,那么争论它很愚蠢是愚蠢的,因为它可以用 45 年历史且不太可读的语言以不同的方式编写。UNIX 管道非常棒且可读,谁在乎是否可以用少一两个管道来编写命令呢? (5认同)
  • @NickMatteo:您可以自由使用您熟悉的任何工具。matigo 似乎对 `grep ... | 很满意 awk ...`,您会对 `awk '/PATTERN/ {do stuff}` 感到满意,而我会对简短的 Ruby 或 Python 脚本感到满意。只要脚本有效且可读,就没有人错,没有命令是愚蠢的,而且客观上肯定不是愚蠢的。 (4认同)

gch*_*bon 13

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

  • (IMO)更容易测试,只需使用pytestunitest模块
  • 对于非 Linux 的人来说它是可读的(当然除了get_deviceLinux 特定的功能......)
  • 更容易上手(同样是IMO)
  • 如果您想发送一些电子邮件怎么办?触发新的行动?使用 Python 等编程语言可以轻松丰富脚本。

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

小问题是我不知道如何轻松获取磁盘 IE 的名称/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()

Run Code Online (Sandbox Code Playgroud)

如果您需要使用 CRON 编排许多任务,则可能值得将一些 Python 代码放在一起作为库,并在许多任务中重用此代码。

编辑:我终于在脚本中添加了 CLI 部分,我想我自己会使用它