fix case with package name which cannot be downloaded

(without special settings)

The issue appears if file or its version contains one of special URI
characters, e.g. +. Theu will be interpreted as query parameters by
(some) servers (e.g. S3 works in this way). In this commit we rename
archive to the one with safe name.
This commit is contained in:
2022-06-27 18:53:48 +03:00
parent fac228d6c6
commit 7b647a9b5a
8 changed files with 90 additions and 24 deletions

View File

@ -52,7 +52,7 @@ class Rebuild(Handler):
if args.from_database:
updates = Rebuild.extract_packages(application)
else:
updates = application.repository.packages_depends_on(depends_on)
updates = application.repository.packages_depend_on(depends_on)
Rebuild.check_if_empty(args.exit_code, not updates)
if args.dry_run:

View File

@ -24,8 +24,9 @@ from typing import Iterable, List, Optional, Set
from ahriman.core.build_tools.task import Task
from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.util import tmpdir
from ahriman.core.util import safe_filename, tmpdir
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
from ahriman.models.result import Result
@ -122,16 +123,16 @@ class Executor(Cleaner):
for local in self.packages():
if local.base in packages or all(package in requested for package in local.packages):
to_remove = {
package: Path(properties.filename)
package: properties.filepath
for package, properties in local.packages.items()
if properties.filename is not None
if properties.filepath is not None
}
remove_base(local.base)
elif requested.intersection(local.packages.keys()):
to_remove = {
package: Path(properties.filename)
package: properties.filepath
for package, properties in local.packages.items()
if package in requested and properties.filename is not None
if package in requested and properties.filepath is not None
}
else:
to_remove = {}
@ -160,17 +161,25 @@ class Executor(Cleaner):
Returns:
Result: path to repository database
"""
def rename(archive: PackageDescription, base: str) -> None:
if archive.filename is None:
self.logger.warning("received empty package name for base %s", base)
return # suppress type checking, it never can be none actually
if (safe := safe_filename(archive.filename)) != archive.filename:
shutil.move(self.paths.packages / archive.filename, self.paths.packages / safe)
archive.filename = safe
def update_single(name: Optional[str], base: str) -> None:
if name is None:
self.logger.warning("received empty package name for base %s", base)
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
full_path = self.paths.packages / name
files = self.sign.process_sign_package(full_path, base)
for src in files:
dst = self.paths.repository / src.name
dst = self.paths.repository / safe_filename(src.name)
shutil.move(src, dst)
package_path = self.paths.repository / name
package_path = self.paths.repository / safe_filename(name)
self.repo.add(package_path)
current_packages = self.packages()
@ -181,6 +190,7 @@ class Executor(Cleaner):
for local in updates:
try:
for description in local.packages.values():
rename(description, local.base)
update_single(description.filename, local.base)
self.reporter.set_success(local)
result.add_success(local)

View File

@ -99,7 +99,7 @@ class Repository(Executor, UpdateHandler):
"""
return list(filter(package_like, self.paths.packages.iterdir()))
def packages_depends_on(self, depends_on: Optional[Iterable[str]]) -> List[Package]:
def packages_depend_on(self, depends_on: Optional[Iterable[str]]) -> List[Package]:
"""
extract list of packages which depends on specified package

View File

@ -20,6 +20,7 @@
import datetime
import io
import os
import re
import requests
import shutil
import subprocess
@ -36,7 +37,7 @@ from ahriman.models.repository_paths import RepositoryPaths
__all__ = ["check_output", "check_user", "exception_response_text", "filter_json", "full_version", "enum_values",
"package_like", "pretty_datetime", "pretty_size", "tmpdir", "walk"]
"package_like", "pretty_datetime", "pretty_size", "safe_filename", "tmpdir", "walk"]
def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None,
@ -272,6 +273,27 @@ def pretty_size(size: Optional[float], level: int = 0) -> str:
return pretty_size(size / 1024, level + 1)
def safe_filename(source: str) -> str:
"""
convert source string to its safe representation
Args:
source(str): string to convert
Returns:
str: result string in which all unsafe characters are replaced by dash
"""
# RFC-3986 https://datatracker.ietf.org/doc/html/rfc3986 states that unreserved characters are
# https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
# however we would like to allow some gen-delims characters in filename, because those characters are used
# as delimiter in other URI parts. The ones we allow are
# ":" - used as separator in schema and userinfo
# "[" and "]" - used for host part
# "@" - used as separator between host and userinfo
return re.sub(r"[^A-Za-z\d\-._~:\[\]@]", "-", source)
@contextmanager
def tmpdir() -> Generator[Path, None, None]:
"""