clean empty directories

This commit is contained in:
2026-02-13 16:53:50 +02:00
parent d0ebd6559c
commit bb31919858
6 changed files with 54 additions and 5 deletions

View File

@@ -19,6 +19,7 @@
# #
import datetime import datetime
from collections.abc import Iterator
from pathlib import Path from pathlib import Path
from ahriman.core.alpm.repo import Repo 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) 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: def repository_for(self, date: datetime.date | None = None) -> Path:
""" """
get full path to repository at the specified date get full path to repository at the specified date
@@ -127,9 +143,12 @@ class ArchiveTree(LazyLogging):
if self._package_symlinks_create(single, root, archive): if self._package_symlinks_create(single, root, archive):
repo.add(root / single.filename) repo.add(root / single.filename)
def symlinks_fix(self) -> None: def symlinks_fix(self) -> Iterator[Path]:
""" """
remove broken symlinks across repositories for all dates 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"): for path in walk(self.paths.archive / "repos"):
root = path.parent root = path.parent
@@ -146,6 +165,7 @@ class ArchiveTree(LazyLogging):
# normally it should be fine to do so # normally it should be fine to do so
package_name = path.name.rsplit("-", maxsplit=3)[0] package_name = path.name.rsplit("-", maxsplit=3)[0]
self._repo(root).remove(package_name, path) self._repo(root).remove(package_name, path)
yield path.parent.relative_to(self.paths.archive / "repos")
def tree_create(self) -> None: def tree_create(self) -> None:
""" """

View File

@@ -66,4 +66,5 @@ class ArchiveTrigger(Trigger):
""" """
trigger action which will be called before the stop of the application 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)

View File

@@ -292,6 +292,7 @@ class RepositoryPaths(LazyLogging):
""" """
for directory in ( for directory in (
self.cache_for(package_base), self.cache_for(package_base),
self.archive_for(package_base),
): ):
shutil.rmtree(directory, ignore_errors=True) shutil.rmtree(directory, ignore_errors=True)

View File

@@ -29,6 +29,25 @@ def test_repository_for(archive_tree: ArchiveTree) -> None:
assert set(map("{:02d}".format, utcnow().timetuple()[:3])).issubset(path.parts) 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, def test_symlinks_create(archive_tree: ArchiveTree, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None: 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") 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") walk_mock.assert_called_once_with(archive_tree.paths.archive / "repos")
remove_mock.assert_called_once_with( remove_mock.assert_called_once_with(
"broken_symlink", archive_tree.repository_for() / "broken_symlink-1.0.0-1-x86_64.pkg.tar.zst") "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") 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() remove_mock.assert_not_called()

View File

@@ -1,3 +1,4 @@
from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.core.archive import ArchiveTrigger 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 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() archive_trigger.on_stop()
symlinks_mock.assert_called_once_with() symlinks_mock.assert_called_once_with()
directories_mock.assert_called_once_with({local})

View File

@@ -271,6 +271,7 @@ def test_tree_clear(repository_paths: RepositoryPaths, package_ahriman: Package,
""" """
paths = { paths = {
repository_paths.cache_for(package_ahriman.base), repository_paths.cache_for(package_ahriman.base),
repository_paths.archive_for(package_ahriman.base),
} }
rmtree_mock = mocker.patch("shutil.rmtree") rmtree_mock = mocker.patch("shutil.rmtree")