From bb31919858d3b45f3d5439c843e1b16fdab6dbfb Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Fri, 13 Feb 2026 16:53:50 +0200 Subject: [PATCH] clean empty directories --- src/ahriman/core/archive/archive_tree.py | 22 +++++++++++++++- src/ahriman/core/archive/archive_trigger.py | 3 ++- src/ahriman/models/repository_paths.py | 1 + .../ahriman/core/archive/test_archive_tree.py | 25 +++++++++++++++++-- .../core/archive/test_archive_trigger.py | 7 +++++- tests/ahriman/models/test_repository_paths.py | 1 + 6 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/ahriman/core/archive/archive_tree.py b/src/ahriman/core/archive/archive_tree.py index 976126f6..2bf8286b 100644 --- a/src/ahriman/core/archive/archive_tree.py +++ b/src/ahriman/core/archive/archive_tree.py @@ -19,6 +19,7 @@ # import datetime +from collections.abc import Iterator from pathlib import Path from ahriman.core.alpm.repo import Repo @@ -84,6 +85,21 @@ class ArchiveTree(LazyLogging): """ return Repo(self.repository_id.name, self.paths, self.sign_args, root) + def directories_fix(self, paths: set[Path]) -> None: + """ + remove empty repository directories recursively + + Args: + paths(set[Path]): repositories to check + """ + root = self.paths.archive / "repos" + for repository in paths: + parents = [repository] + list(repository.parents[:-1]) + for parent in parents: + path = root / parent + if not list(path.iterdir()): + path.rmdir() + def repository_for(self, date: datetime.date | None = None) -> Path: """ get full path to repository at the specified date @@ -127,9 +143,12 @@ class ArchiveTree(LazyLogging): if self._package_symlinks_create(single, root, archive): repo.add(root / single.filename) - def symlinks_fix(self) -> None: + def symlinks_fix(self) -> Iterator[Path]: """ remove broken symlinks across repositories for all dates + + Yields: + Path: path of the sub-repository with removed symlinks """ for path in walk(self.paths.archive / "repos"): root = path.parent @@ -146,6 +165,7 @@ class ArchiveTree(LazyLogging): # normally it should be fine to do so package_name = path.name.rsplit("-", maxsplit=3)[0] self._repo(root).remove(package_name, path) + yield path.parent.relative_to(self.paths.archive / "repos") def tree_create(self) -> None: """ diff --git a/src/ahriman/core/archive/archive_trigger.py b/src/ahriman/core/archive/archive_trigger.py index dfe21cc6..0d24b5f4 100644 --- a/src/ahriman/core/archive/archive_trigger.py +++ b/src/ahriman/core/archive/archive_trigger.py @@ -66,4 +66,5 @@ class ArchiveTrigger(Trigger): """ trigger action which will be called before the stop of the application """ - self.tree.symlinks_fix() + repositories = set(self.tree.symlinks_fix()) + self.tree.directories_fix(repositories) diff --git a/src/ahriman/models/repository_paths.py b/src/ahriman/models/repository_paths.py index 0562c6ad..4d4daff6 100644 --- a/src/ahriman/models/repository_paths.py +++ b/src/ahriman/models/repository_paths.py @@ -292,6 +292,7 @@ class RepositoryPaths(LazyLogging): """ for directory in ( self.cache_for(package_base), + self.archive_for(package_base), ): shutil.rmtree(directory, ignore_errors=True) diff --git a/tests/ahriman/core/archive/test_archive_tree.py b/tests/ahriman/core/archive/test_archive_tree.py index 9b689d0f..095ff602 100644 --- a/tests/ahriman/core/archive/test_archive_tree.py +++ b/tests/ahriman/core/archive/test_archive_tree.py @@ -29,6 +29,25 @@ def test_repository_for(archive_tree: ArchiveTree) -> None: assert set(map("{:02d}".format, utcnow().timetuple()[:3])).issubset(path.parts) +def test_directories_fix(archive_tree: ArchiveTree, mocker: MockerFixture) -> None: + """ + must remove empty directories recursively + """ + root = archive_tree.paths.archive / "repos" + (root / "a" / "b").mkdir(parents=True, exist_ok=True) + (root / "a" / "b" / "file").touch() + (root / "a" / "b" / "c" / "d").mkdir(parents=True, exist_ok=True) + + _original_rmdir = Path.rmdir + rmdir_mock = mocker.patch("pathlib.Path.rmdir", autospec=True, side_effect=_original_rmdir) + + archive_tree.directories_fix({Path("a") / "b" / "c" / "d"}) + rmdir_mock.assert_has_calls([ + MockCall(root / "a" / "b" / "c" / "d"), + MockCall(root / "a" / "b" / "c"), + ]) + + def test_symlinks_create(archive_tree: ArchiveTree, package_ahriman: Package, package_python_schedule: Package, mocker: MockerFixture) -> None: """ @@ -96,7 +115,9 @@ def test_symlinks_fix(archive_tree: ArchiveTree, mocker: MockerFixture) -> None: ]) remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove") - archive_tree.symlinks_fix() + assert list(archive_tree.symlinks_fix()) == [ + archive_tree.repository_for().relative_to(archive_tree.paths.archive / "repos"), + ] walk_mock.assert_called_once_with(archive_tree.paths.archive / "repos") remove_mock.assert_called_once_with( "broken_symlink", archive_tree.repository_for() / "broken_symlink-1.0.0-1-x86_64.pkg.tar.zst") @@ -125,7 +146,7 @@ def test_symlinks_fix_foreign_repository(archive_tree: ArchiveTree, mocker: Mock ]) remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove") - archive_tree.symlinks_fix() + assert list(archive_tree.symlinks_fix()) == [] remove_mock.assert_not_called() diff --git a/tests/ahriman/core/archive/test_archive_trigger.py b/tests/ahriman/core/archive/test_archive_trigger.py index bdacc11b..6c92912c 100644 --- a/tests/ahriman/core/archive/test_archive_trigger.py +++ b/tests/ahriman/core/archive/test_archive_trigger.py @@ -1,3 +1,4 @@ +from pathlib import Path from pytest_mock import MockerFixture from ahriman.core.archive import ArchiveTrigger @@ -27,6 +28,10 @@ def test_on_stop(archive_trigger: ArchiveTrigger, mocker: MockerFixture) -> None """ must fix broken symlinks on stop """ - symlinks_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.symlinks_fix") + local = Path("local") + symlinks_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.symlinks_fix", return_value=[local]) + directories_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.directories_fix") + archive_trigger.on_stop() symlinks_mock.assert_called_once_with() + directories_mock.assert_called_once_with({local}) diff --git a/tests/ahriman/models/test_repository_paths.py b/tests/ahriman/models/test_repository_paths.py index 31635729..ca1e4753 100644 --- a/tests/ahriman/models/test_repository_paths.py +++ b/tests/ahriman/models/test_repository_paths.py @@ -271,6 +271,7 @@ def test_tree_clear(repository_paths: RepositoryPaths, package_ahriman: Package, """ paths = { repository_paths.cache_for(package_ahriman.base), + repository_paths.archive_for(package_ahriman.base), } rmtree_mock = mocker.patch("shutil.rmtree")