diff --git a/config/packages/packeton.yaml b/config/packages/packeton.yaml index ae2a8bb4..a7eb705f 100644 --- a/config/packages/packeton.yaml +++ b/config/packages/packeton.yaml @@ -1,7 +1,7 @@ packeton: github_no_api: '%env(bool:GITHUB_NO_API)%' rss_max_items: 30 - archive: true + archive: ['mirror', 'replace'] anonymous_access: '%env(bool:PUBLIC_ACCESS)%' anonymous_archive_access: '%env(bool:PUBLIC_ACCESS)%' archive_options: diff --git a/config/services.yaml b/config/services.yaml index 90e7c0e5..c0d35665 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -37,11 +37,11 @@ parameters: use_forward: false check_path: /login failure_path: null - + composer_home_dir: '%env(resolve:APP_COMPOSER_HOME)%' mirror_repos_meta_dir: '%composer_home_dir%/proxy-meta' mirror_repos_dist_dir: '%composer_home_dir%/proxy-dist' - + package_name_regex: '[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+' package_name_regex_v2: '[A-Za-z0-9_.-]+/[A-Za-z0-9_.~-]+' package_name_regex_v1: '[A-Za-z0-9_.-]+/[A-Za-z0-9_.$-]+' # package$hash @@ -103,7 +103,7 @@ services: Packeton\Resolver\ControllerArgumentResolver: tags: - { name: controller.argument_value_resolver, priority: 105 } - + Packeton\Service\AssetHashVersionStrategy: arguments: $publicDir: '%kernel.project_dir%/public' @@ -114,16 +114,12 @@ services: $isAnonymousAccess: '%anonymous_access%' $isAnonymousArchiveAccess: '%anonymous_archive_access%' $isAnonymousMirror: '%anonymous_mirror_access%' - + Packeton\Model\UploadZipballStorage: arguments: $supportTypes: '%packeton_artifact_types%' $tmpDir: '%packeton_artifact_storage%' - Packeton\Service\DistConfig: - arguments: - $config: '%packeton_archive_opts%' - Packeton\Package\InMemoryDumper: arguments: $config: '%packeton_dumper_opts%' diff --git a/src/Controller/ZipballController.php b/src/Controller/ZipballController.php index ad032848..72be2d84 100644 --- a/src/Controller/ZipballController.php +++ b/src/Controller/ZipballController.php @@ -13,6 +13,7 @@ use Packeton\Package\RepTypes; use Packeton\Service\DistManager; use Packeton\Util\PacketonUtils; +use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\JsonResponse; @@ -30,7 +31,8 @@ public function __construct( protected DistManager $dm, protected UploadZipballStorage $storage, protected ManagerRegistry $registry, - protected EventDispatcherInterface $dispatcher + protected EventDispatcherInterface $dispatcher, + protected LoggerInterface $logger, ) { } @@ -80,7 +82,7 @@ public function zipballList(Request $request): Response #[Route( '/zipball/{package}/{hash}', name: 'download_dist_package', - requirements: ['package' => '%package_name_regex%', 'hash' => '[a-f0-9]{40}\.[a-z]+?'], + requirements: ['package' => '%package_name_regex%', 'hash' => '[a-f0-9]{40}(\.?[A-Za-z\.]+?)?'], methods: ['GET'] )] public function zipballAction(#[Vars('name')] Package $package, string $hash): Response @@ -103,6 +105,7 @@ public function zipballAction(#[Vars('name')] Package $package, string $hash): R $dist = $this->dm->getDist($reference, $package); } catch (\Exception $e) { $msg = $this->isGranted('ROLE_MAINTAINER') ? $e->getMessage() : null; + $this->logger->warning($e->getMessage(), ['e' => $e]); return $this->createNotFound($msg); } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 9a75b75b..e2c7b2ed 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -6,6 +6,7 @@ use Packeton\Composer\MetadataFormat; use Packeton\Integrations\Factory\OAuth2FactoryInterface; use Packeton\Integrations\Model\AppUtils; +use Packeton\Service\DistConfig; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; @@ -30,6 +31,18 @@ public function getConfigTreeBuilder(): TreeBuilder $treeBuilder = new TreeBuilder('packeton'); $rootNode = $treeBuilder->getRootNode(); + $archiveNormalizer = static function(mixed $value): bool|array { + $allFlags = [DistConfig::FLAG_REPLACE, DistConfig::FLAG_MIRROR]; + if ($value === true) { + return $allFlags; + } + $value = is_string($value) ? [$value] : $value; + if (is_array($value) && $diff = array_diff($value, $allFlags)) { + throw new \InvalidArgumentException(sprintf('packeton->archive support only [mirror, replace] options, but given %s', json_encode($diff))); + } + return is_array($value) ? $value : []; + }; + $rootNode ->children() ->booleanNode('github_no_api')->end() @@ -57,7 +70,8 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->booleanNode('health_check')->defaultTrue()->end() ->integerNode('max_import')->end() - ->booleanNode('archive') + ->variableNode('archive') + ->beforeNormalization()->always()->then($archiveNormalizer)->end() ->defaultFalse() ->end() ->arrayNode('artifacts') @@ -104,7 +118,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->validate() ->always(function ($values) { if (($values['archive'] ?? false) && !isset($values['archive_options'])) { - throw new \InvalidArgumentException('archive_options is required if archive: true'); + throw new \InvalidArgumentException('archive_options is required if packeton->archive is not false'); } return $values; diff --git a/src/DependencyInjection/PacketonExtension.php b/src/DependencyInjection/PacketonExtension.php index 22590b97..a7df0c2c 100644 --- a/src/DependencyInjection/PacketonExtension.php +++ b/src/DependencyInjection/PacketonExtension.php @@ -59,8 +59,10 @@ public function load(array $configs, ContainerBuilder $container): void $config = $this->processConfiguration($configuration, $configs); $container->setParameter('packagist_web.rss_max_items', $config['rss_max_items']); - $container->setParameter('packeton_archive_all_opts', $config['archive_options'] ?? []); - $container->setParameter('packeton_archive_opts', true === $config['archive'] ? $container->getParameter('packeton_archive_all_opts') : []); + + $archiveOptions = $config['archive_options'] ?? []; + $archiveOptions['flags'] = $config['archive'] ?? []; + $container->setParameter('packeton_archive_opts', $archiveOptions); $container->setParameter('packeton_dumper_opts', $config['metadata'] ?? []); diff --git a/src/Package/InMemoryDumper.php b/src/Package/InMemoryDumper.php index 9575cf86..c738f5ca 100644 --- a/src/Package/InMemoryDumper.php +++ b/src/Package/InMemoryDumper.php @@ -15,6 +15,7 @@ use Packeton\Repository\PackageRepository; use Packeton\Repository\VersionRepository; use Packeton\Security\Acl\PackagesAclChecker; +use Packeton\Service\DistConfig; use Packeton\Service\SubRepositoryHelper; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -29,6 +30,7 @@ public function __construct( private readonly PackagesAclChecker $checker, private readonly RouterInterface $router, private readonly SubRepositoryHelper $subRepositoryHelper, + private readonly DistConfig $distConfig, ?array $config = null, ) { $this->infoMessage = $config['info_cmd_message'] ?? null; @@ -103,6 +105,12 @@ private function dumpRootPackages(?UserInterface $user = null, ?int $apiVersion $rootFile['metadata-changes-url'] = $this->router->generate('metadata_changes'); $rootFile['providers-url'] = $slug . '/p/%package%$%hash%.json'; + if ($this->distConfig->mirrorEnabled()) { + $ref = '0000000000000000000000000000000000000000.zip'; + $zipball = $this->router->generate('download_dist_package', ['package' => 'VND/PKG', 'hash' => $ref]); + $rootFile['mirrors'][] = ['dist-url' => \str_replace(['VND/PKG', $ref], ['%package%', '%reference%.%type%'], $zipball), 'preferred' => true]; + } + $rootFile['metadata-url'] = $slug . '/p2/%package%.json'; if ($subRepo) { $rootFile['_comment'] = "Subrepository {$subRepo->getSlug()}"; diff --git a/src/Package/Updater.php b/src/Package/Updater.php index 4edf9aba..61bf69b9 100644 --- a/src/Package/Updater.php +++ b/src/Package/Updater.php @@ -573,7 +573,7 @@ private function updateArchive(PackageInterface $data, Package $package): ?array ]; } - if ($this->distConfig->isEnable() === false) { + if (false === $this->distConfig->replaceEnabled()) { if ($data->getDistUrl()) { return [ 'url' => $data->getDistUrl(), @@ -581,7 +581,6 @@ private function updateArchive(PackageInterface $data, Package $package): ?array 'reference' => $data->getDistReference(), ]; } - return null; } diff --git a/src/Service/DistConfig.php b/src/Service/DistConfig.php index e875dbdd..5c53e1ab 100644 --- a/src/Service/DistConfig.php +++ b/src/Service/DistConfig.php @@ -5,22 +5,24 @@ namespace Packeton\Service; use Symfony\Component\Routing\RouterInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; class DistConfig { public const HOSTNAME_PLACEHOLDER = '__host_unset__'; - private $config; - private $router; + public const FLAG_MIRROR = 'mirror'; + public const FLAG_REPLACE = 'replace'; /** * @param RouterInterface $router * @param array $config */ - public function __construct(RouterInterface $router, array $config) - { - $this->config = $config; - $this->router = $router; + public function __construct( + private readonly RouterInterface $router, + #[Autowire('%packeton_archive_opts%')] + private readonly array $config + ) { } public function generateTargetDir(string $name) @@ -138,12 +140,19 @@ public function generateRoute(string $name, string $reference, ?string $format = return $hostName . $uri; } - /** - * @return bool - */ - public function isEnable(): bool + public function archiveEnabled(): bool + { + return !empty($this->config['flags']); + } + + public function mirrorEnabled(): bool + { + return in_array(self::FLAG_MIRROR, $this->config['flags'] ?? []); + } + + public function replaceEnabled(): bool { - return !empty($this->config); + return in_array(self::FLAG_REPLACE, $this->config['flags'] ?? []); } /** diff --git a/src/Service/DistManager.php b/src/Service/DistManager.php index 67476a92..1bf417de 100644 --- a/src/Service/DistManager.php +++ b/src/Service/DistManager.php @@ -81,7 +81,7 @@ public function buildArchive(string $reference, Package $package, Version|string public function isEnabled(): bool { - return $this->config->isEnable(); + return $this->config->archiveEnabled(); } public function downloadUsingIntegration(string $reference, Package $package, ?string $versionName = null): string