feat: archive package tree implementation (#153)

* store built packages in archive tree instead of repository

* write tests to support new changes

* implement atomic_move method, move files only with lock

* use generic packages tree for all repos

* lookup through archive packages before build

* add archive trigger

* add archive trigger

* regenerate docs

* gpg loader fix

* support requires repostory flag

* drop excess REQUIRES_REPOSITORY

* simplify symlionk creation

* remove generators

* fix sttyle

* add separate function for symlinks creation

* fix rebase

* add note about slicing

* smol refactoring of archive_tree class

* remove duplicate code

* fix typos

* few review fixes

* monor fixes and typos

* clean empty directories

* remove side effect from getter

* drop recursive remove

* ensure_exists now accepts only argument

* add package like guard to symlinks fix

* speedup archive_lookup processing by iterrupting cycle

* remove custom filelock

* fix naming

* remove remove flag from repo

* review fixes

* restore wrapper around filelock

* extract repository explorer to separate class

* docs update

* fix ide findings
This commit is contained in:
2026-02-16 00:12:51 +02:00
committed by GitHub
parent 6a2454548d
commit 2d6d42f969
71 changed files with 1876 additions and 363 deletions

View File

@@ -66,7 +66,7 @@ async def test_delete_partially(client: TestClient, package_ahriman: Package) ->
assert json
async def test_delete_exception(client: TestClient, package_ahriman: Package) -> None:
async def test_delete_exception(client: TestClient) -> None:
"""
must raise exception on invalid payload
"""

View File

@@ -109,7 +109,7 @@ async def test_post(client: TestClient, repository_paths: RepositoryPaths, mocke
local = Path("local")
save_mock = pytest.helpers.patch_view(client.app, "save_file",
AsyncMock(return_value=("filename", local / ".filename")))
rename_mock = mocker.patch("pathlib.Path.rename")
rename_mock = mocker.patch("ahriman.web.views.v1.service.upload.atomic_move")
# no content validation here because it has invalid schema
data = FormData()
@@ -118,7 +118,7 @@ async def test_post(client: TestClient, repository_paths: RepositoryPaths, mocke
response = await client.post("/api/v1/service/upload", data=data)
assert response.ok
save_mock.assert_called_once_with(pytest.helpers.anyvar(int), repository_paths.packages, max_body_size=None)
rename_mock.assert_called_once_with(local / "filename")
rename_mock.assert_called_once_with(local / ".filename", local / "filename")
async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
@@ -131,7 +131,7 @@ async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPat
("filename", local / ".filename"),
("filename.sig", local / ".filename.sig"),
]))
rename_mock = mocker.patch("pathlib.Path.rename")
rename_mock = mocker.patch("ahriman.web.views.v1.service.upload.atomic_move")
# no content validation here because it has invalid schema
data = FormData()
@@ -145,8 +145,8 @@ async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPat
MockCall(pytest.helpers.anyvar(int), repository_paths.packages, max_body_size=None),
])
rename_mock.assert_has_calls([
MockCall(local / "filename"),
MockCall(local / "filename.sig"),
MockCall(local / ".filename", local / "filename"),
MockCall(local / ".filename.sig", local / "filename.sig"),
])