Browse Source

add command-line param `--delete-path-regex`

first part of https://github.com/fphammerle/free-disk/pull/62
Fabian Peter Hammerle 1 year ago
parent
commit
80363880bc
4 changed files with 116 additions and 5 deletions
  1. 3 0
      CHANGELOG.md
  2. 1 0
      README.md
  3. 12 1
      free_disk/__init__.py
  4. 100 4
      tests/test_cleanup.py

+ 3 - 0
CHANGELOG.md

@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+### Added
+- param `--delete-path-regex`
+
 ### Changed
 - made all constants and functions private
 

+ 1 - 0
README.md

@@ -19,6 +19,7 @@ pip3 install --user --upgrade free-disk
 ```sh
 free-disk --help
 free-disk --free-bytes 1GiB /dir/to/cleanup
+free-disk --free-bytes 1GiB --delete-path-regex '\.mp4$' /dir/to/cleanup
 free-disk --debug --free-bytes 2GB /dir/to/cleanup
 ```
 

+ 12 - 1
free_disk/__init__.py

@@ -44,6 +44,15 @@ def _data_size_to_bytes(size_with_unit: str) -> int:
 def _main() -> None:
     argparser = argparse.ArgumentParser(description=__doc__)
     argparser.add_argument("-d", "--debug", action="store_true")
+    argparser.add_argument(
+        "--delete-path-regex",
+        metavar="REGULAR_EXPRESSION",
+        type=re.compile,  # type: ignore
+        help="Only delete files with path matching regular expression (at any position)."
+        " Paths will not be resolved or made absolute before check."
+        r" Examples: \.mp4$ or ^/tmp/\d or ^rel/ative/ (default: no filter)",
+        default="",
+    )
     argparser.add_argument(
         "--free-bytes",
         type=_data_size_to_bytes,
@@ -68,7 +77,9 @@ def _main() -> None:
         for dirpath, _, filenames in os.walk(args.root_dir_path)
         for filename in filenames
     ]
-    file_mtime_paths = [(os.stat(p).st_mtime, p) for p in file_paths]
+    file_mtime_paths = [
+        (os.stat(p).st_mtime, p) for p in file_paths if args.delete_path_regex.search(p)
+    ]
     file_mtime_paths.sort()
     removed_files_counter = 0
     last_mtime = None

+ 100 - 4
tests/test_cleanup.py

@@ -1,5 +1,6 @@
 import collections
 import logging
+import os
 import pathlib
 import unittest.mock
 
@@ -11,8 +12,8 @@ import free_disk
 _DiskUsage = collections.namedtuple("_DiskUsage", ("free",))
 
 
-def _folder_size_bytes(path: pathlib.Path) -> int:
-    return sum(p.stat().st_size for p in path.rglob("*"))
+def _folder_content_size_bytes(path: pathlib.Path) -> int:
+    return sum(p.stat().st_size for p in path.rglob("*") if p.is_file())
 
 
 def test__main_remove_some(
@@ -25,7 +26,7 @@ def test__main_remove_some(
     tmp_path.joinpath("e").write_bytes(b"d" * 7)
     with unittest.mock.patch(
         "shutil.disk_usage",
-        lambda p: _DiskUsage(free=42 - _folder_size_bytes(tmp_path)),
+        lambda p: _DiskUsage(free=42 - _folder_content_size_bytes(tmp_path)),
     ), unittest.mock.patch(
         "sys.argv", ["", "--free-bytes", "30B", str(tmp_path)]
     ), caplog.at_level(
@@ -49,6 +50,38 @@ def test__main_remove_some(
     )
 
 
+def test__main_remove_from_subfolder(
+    caplog: _pytest.logging.LogCaptureFixture, tmp_path: pathlib.Path
+) -> None:
+    tmp_path.joinpath("a").mkdir()
+    tmp_path.joinpath("a", "aa").write_bytes(b"a" * 4)
+    tmp_path.joinpath("b").write_bytes(b"b" * 3)
+    tmp_path.joinpath("c").write_bytes(b"c" * 5)
+    with unittest.mock.patch(
+        "shutil.disk_usage",
+        lambda p: _DiskUsage(free=42 - _folder_content_size_bytes(tmp_path)),
+    ), unittest.mock.patch(
+        "sys.argv", ["", "--free-bytes", "35B", str(tmp_path)]
+    ), caplog.at_level(
+        logging.DEBUG
+    ):
+        free_disk._main()
+    assert {p.name for p in tmp_path.rglob("*")} == {"a", "c"}
+    assert caplog.record_tuples[:-1] == [
+        ("root", logging.DEBUG, m)
+        for m in [
+            "Required free bytes: 35",
+            "_DiskUsage(free=30)",
+            f"Removed file {tmp_path.joinpath('a', 'aa')}",
+            f"Removed file {tmp_path.joinpath('b')}",
+        ]
+    ]
+    assert caplog.records[-1].levelno == logging.INFO
+    assert caplog.records[-1].message.startswith(
+        "Removed 2 file(s) with modification date <= 20"
+    )
+
+
 def test__main_sufficient_space(
     caplog: _pytest.logging.LogCaptureFixture, tmp_path: pathlib.Path
 ) -> None:
@@ -57,7 +90,7 @@ def test__main_sufficient_space(
     tmp_path.joinpath("c").write_bytes(b"c" * 5)
     with unittest.mock.patch(
         "shutil.disk_usage",
-        lambda p: _DiskUsage(free=42 - _folder_size_bytes(tmp_path)),
+        lambda p: _DiskUsage(free=42 - _folder_content_size_bytes(tmp_path)),
     ), unittest.mock.patch(
         "sys.argv", ["", "--free-bytes", "30B", str(tmp_path)]
     ), caplog.at_level(
@@ -88,3 +121,66 @@ def test__main_no_files(
         ("root", logging.DEBUG, "_DiskUsage(free=21)"),
         ("root", logging.WARNING, "No files to remove"),
     ]
+
+
+def test__main_path_regex_absolute(
+    caplog: _pytest.logging.LogCaptureFixture, tmp_path: pathlib.Path
+) -> None:
+    tmp_path.joinpath("a").mkdir()
+    tmp_path.joinpath("a", "aa").write_bytes(b"aa")
+    tmp_path.joinpath("a", "a~").write_bytes(b"a~")
+    tmp_path.joinpath("b").write_bytes(b"b")
+    tmp_path.joinpath("c").write_bytes(b"c")
+    tmp_path.joinpath("d").write_bytes(b"d")
+    with unittest.mock.patch(
+        "shutil.disk_usage",
+        lambda p: _DiskUsage(free=42 - _folder_content_size_bytes(tmp_path)),
+    ), unittest.mock.patch(
+        "sys.argv",
+        [
+            "",
+            "--free-bytes",
+            "42B",
+            str(tmp_path),
+            "--delete-path-regex",
+            r"a/a|^b|c$|^.*/d$",
+        ],
+    ), caplog.at_level(
+        logging.INFO
+    ):
+        free_disk._main()
+    assert {p.name for p in tmp_path.rglob("*")} == {"a", "b"}
+    assert caplog.records[-1].message.startswith(
+        "Removed 4 file(s) with modification date <= 20"
+    )
+
+
+def test__main_path_regex_relative(
+    caplog: _pytest.logging.LogCaptureFixture, tmp_path: pathlib.Path
+) -> None:
+    tmp_path.joinpath("a").mkdir()
+    tmp_path.joinpath("a", "aa").write_bytes(b"aa")
+    tmp_path.joinpath("a", "aaa").write_bytes(b"aaa")
+    tmp_path.joinpath("a", "A").write_bytes(b"A")
+    tmp_path.joinpath("b").write_bytes(b"b")
+    tmp_path.joinpath("b2").write_bytes(b"b2")
+    tmp_path.joinpath("c").write_bytes(b"c")
+    with unittest.mock.patch(
+        "shutil.disk_usage",
+        lambda p: _DiskUsage(free=42 - _folder_content_size_bytes(tmp_path)),
+    ), unittest.mock.patch(
+        "sys.argv",
+        ["", "--free-bytes", "123B", ".", "--delete-path-regex", r"/aa|^b|\d$|^\./c$"],
+    ), caplog.at_level(
+        logging.INFO
+    ):
+        old_working_dir = os.getcwd()
+        try:
+            os.chdir(tmp_path)
+            free_disk._main()
+        finally:
+            os.chdir(old_working_dir)
+    assert {p.name for p in tmp_path.rglob("*")} == {"a", "A", "b"}
+    assert caplog.records[-1].message.startswith(
+        "Removed 4 file(s) with modification date <= 20"
+    )