add archive trigger

This commit is contained in:
2025-08-14 13:32:55 +03:00
parent c89f6ad98c
commit e6df656ce3
8 changed files with 153 additions and 15 deletions

View File

@ -1,5 +1,6 @@
[build] [build]
; List of well-known triggers. Used only for configuration purposes. ; List of well-known triggers. Used only for configuration purposes.
triggers_known[] = ahriman.core.archive.ArchiveTrigger
triggers_known[] = ahriman.core.distributed.WorkerLoaderTrigger triggers_known[] = ahriman.core.distributed.WorkerLoaderTrigger
triggers_known[] = ahriman.core.distributed.WorkerTrigger triggers_known[] = ahriman.core.distributed.WorkerTrigger
triggers_known[] = ahriman.core.support.KeyringTrigger triggers_known[] = ahriman.core.support.KeyringTrigger

View File

@ -88,22 +88,24 @@ class Repo(LazyLogging):
check_output("repo-add", *self.sign_args, str(self.repo_path), check_output("repo-add", *self.sign_args, str(self.repo_path),
cwd=self.root, logger=self.logger, user=self.uid) cwd=self.root, logger=self.logger, user=self.uid)
def remove(self, package: str, filename: Path) -> None: def remove(self, package_name: str | None, filename: Path) -> None:
""" """
remove package from repository remove package from repository
Args: Args:
package(str): package name to remove package_name(str | None): package name to remove. If none set, it will be guessed from filename
filename(Path): package filename to remove filename(Path): package filename to remove
""" """
package_name = package_name or filename.name.rsplit("-", maxsplit=3)[0]
# remove package and signature (if any) from filesystem # remove package and signature (if any) from filesystem
for full_path in self.root.glob(f"**/{filename.name}*"): for full_path in self.root.glob(f"**/{filename.name}*"):
full_path.unlink() full_path.unlink()
# remove package from registry # remove package from registry
check_output( check_output(
"repo-remove", *self.sign_args, str(self.repo_path), package, "repo-remove", *self.sign_args, str(self.repo_path), package_name,
exception=BuildError.from_process(package), exception=BuildError.from_process(package_name),
cwd=self.root, cwd=self.root,
logger=self.logger, logger=self.logger,
user=self.uid, user=self.uid,

View File

@ -17,3 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from ahriman.core.archive.archive_trigger import ArchiveTrigger

View File

@ -0,0 +1,127 @@
#
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import datetime
from pathlib import Path
from ahriman.core.alpm.repo import Repo
from ahriman.core.log import LazyLogging
from ahriman.core.utils import utcnow
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
class ArchiveTree(LazyLogging):
"""
wrapper around archive tree
Attributes:
paths(RepositoryPaths): repository paths instance
repository_id(RepositoryId): repository unique identifier
sign_args(list[str]): additional args which have to be used to sign repository archive
"""
def __init__(self, repository_path: RepositoryPaths, sign_args: list[str]) -> None:
"""
Args:
repository_path(RepositoryPaths): repository paths instance
sign_args(list[str]): additional args which have to be used to sign repository archive
"""
self.paths = repository_path
self.repository_id = repository_path.repository_id
self.sign_args = sign_args
def repository_for(self, date: datetime.date | None = None) -> Path:
"""
get full path to repository at the specified date
Args:
date(datetime.date | None, optional): date to generate path. If none supplied then today will be used
Returns:
Path: path to the repository root
"""
date = date or utcnow().today()
return (
self.paths.archive
/ "repos"
/ date.strftime("%Y")
/ date.strftime("%m")
/ date.strftime("%d")
/ self.repository_id.name
/ self.repository_id.architecture
)
def symlinks_create(self, packages: list[Package]) -> None:
"""
create symlinks for the specified packages in today's repository
Args:
packages(list[Package]): list of packages to be updated
"""
root = self.repository_for()
repo = Repo(self.repository_id.name, self.paths, self.sign_args, root)
for package in packages:
archive = self.paths.archive_for(package.base)
for package_name, single in package.packages.items():
if single.filename is None:
self.logger.warning("received empty package filename for %s", package_name)
continue
has_file = False
for file in archive.glob(f"{single.filename}*"):
if not (symlink := root / file.name).exists():
has_file |= True
symlink.symlink_to(file.relative_to(symlink.parent, walk_up=True))
if has_file:
repo.add(root / single.filename)
def symlinks_fix(self) -> None:
"""
remove broken symlinks across all repositories
"""
for root, _, files in self.paths.archive.walk():
*_, name, architecture = root.parts
if self.repository_id.name != name or self.repository_id.architecture != architecture:
continue # we only process same name repositories
for file in files:
path = root / file
if not path.is_symlink():
continue # find symlinks only
if path.exists():
continue # filter out not broken symlinks
repo = Repo(self.repository_id.name, self.paths, self.sign_args, root)
repo.remove(None, path)
def tree_create(self) -> None:
"""
create repository tree for current repository
"""
path = self.repository_for()
if path.exists():
return
with self.paths.preserve_owner(self.paths.archive):
path.mkdir(0o755, parents=True)

View File

@ -17,16 +17,17 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from pathlib import Path from ahriman.core import context
from ahriman.core.archive.archive_tree import ArchiveTree
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.sign.gpg import GPG
from ahriman.core.triggers import Trigger from ahriman.core.triggers import Trigger
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
from ahriman.models.result import Result from ahriman.models.result import Result
class ArchiveRotationTrigger(Trigger): class ArchiveTrigger(Trigger):
""" """
archive repository extension archive repository extension
@ -44,9 +45,8 @@ class ArchiveRotationTrigger(Trigger):
self.paths = configuration.repository_paths self.paths = configuration.repository_paths
@property ctx = context.get()
def repos_path(self) -> Path: self.tree = ArchiveTree(self.paths, ctx.get(GPG).repository_sign_args)
return self.paths.archive / "repos"
def on_result(self, result: Result, packages: list[Package]) -> None: def on_result(self, result: Result, packages: list[Package]) -> None:
""" """
@ -56,10 +56,16 @@ class ArchiveRotationTrigger(Trigger):
result(Result): build result result(Result): build result
packages(list[Package]): list of all available packages packages(list[Package]): list of all available packages
""" """
self.tree.symlinks_create(packages)
def on_start(self) -> None: def on_start(self) -> None:
""" """
trigger action which will be called at the start of the application trigger action which will be called at the start of the application
""" """
with self.paths.preserve_owner(self.repos_path): self.tree.tree_create()
self.repos_path.mkdir(mode=0o755, exist_ok=True)
def on_stop(self) -> None:
"""
trigger action which will be called before the stop of the application
"""
self.tree.symlinks_fix()

View File

@ -80,7 +80,7 @@ class Executor(PackageInfo, Cleaner):
package_base(str): package base name package_base(str): package base name
""" """
if description.filename is None: if description.filename is None:
self.logger.warning("received empty package name for base %s", package_base) self.logger.warning("received empty package filename for base %s", package_base)
return # suppress type checking, it never can be none actually return # suppress type checking, it never can be none actually
if (safe := safe_filename(description.filename)) != description.filename: if (safe := safe_filename(description.filename)) != description.filename:
@ -161,7 +161,7 @@ class Executor(PackageInfo, Cleaner):
packager_key(str | None): packager key identifier packager_key(str | None): packager key identifier
""" """
if filename is None: if filename is None:
self.logger.warning("received empty package name for base %s", package_base) self.logger.warning("received empty package filename for base %s", package_base)
return # suppress type checking, it never can be none actually return # suppress type checking, it never can be none actually
# in theory, it might be NOT packages directory, but we suppose it is # in theory, it might be NOT packages directory, but we suppose it is

View File

@ -37,6 +37,7 @@ SUBPACKAGES = {
"ahriman-triggers": [ "ahriman-triggers": [
prefix / "share" / "ahriman" / "settings" / "ahriman.ini.d" / "00-triggers.ini", prefix / "share" / "ahriman" / "settings" / "ahriman.ini.d" / "00-triggers.ini",
site_packages / "ahriman" / "application" / "handlers" / "triggers_support.py", site_packages / "ahriman" / "application" / "handlers" / "triggers_support.py",
site_packages / "ahriman" / "core" / "archive",
site_packages / "ahriman" / "core" / "distributed", site_packages / "ahriman" / "core" / "distributed",
site_packages / "ahriman" / "core" / "support", site_packages / "ahriman" / "core" / "support",
], ],