diff --git a/src/Composer/PacketonRepositoryFactory.php b/src/Composer/PacketonRepositoryFactory.php index ee8a654c..23167554 100644 --- a/src/Composer/PacketonRepositoryFactory.php +++ b/src/Composer/PacketonRepositoryFactory.php @@ -41,7 +41,7 @@ public function create(array $repoConfig, IOInterface $io, Config $config, ?stri return match ($type) { RepTypes::ARTIFACT => new ArtifactRepository($repoConfig, $this->zipballStorage, $this->registry, $io, $config, $httpDownloader), - RepTypes::CUSTOM => new CustomJsonRepository($repoConfig, $this->registry, $io, $config, $httpDownloader), + RepTypes::CUSTOM, RepTypes::VIRTUAL => new CustomJsonRepository($repoConfig, $this->registry, $io, $config, $httpDownloader), default => new VcsRepository($repoConfig, $io, $config, $httpDownloader, $this->driverFactory, null, $process), }; } diff --git a/src/Composer/Repository/CustomJsonRepository.php b/src/Composer/Repository/CustomJsonRepository.php index b21985ff..04717455 100644 --- a/src/Composer/Repository/CustomJsonRepository.php +++ b/src/Composer/Repository/CustomJsonRepository.php @@ -14,6 +14,7 @@ use Composer\Util\ProcessExecutor; use Doctrine\Persistence\ManagerRegistry; use Packeton\Entity\Zipball; +use Packeton\Package\RepTypes; use Packeton\Service\DistConfig; class CustomJsonRepository extends ArrayRepository implements PacketonRepositoryInterface @@ -128,6 +129,12 @@ protected function loadVersion(array $version): BasePackage 'reference' => $dist->getReference(), 'url' => DistConfig::HOSTNAME_PLACEHOLDER, ]; + } elseif (($this->repoConfig['repoType'] ?? null) === RepTypes::VIRTUAL) { + $data['dist'] = [ + 'type' => 'zip', + 'reference' => sha1(json_encode($data)), + 'url' => DistConfig::HOSTNAME_PLACEHOLDER, + ]; } if (($url = $version['dist']['url'] ?? null) && preg_match('{^(\.|[a-z]:|/)}i', $url)) { diff --git a/src/Controller/Api/ApiController.php b/src/Controller/Api/ApiController.php index fcc3b25e..39378bb6 100644 --- a/src/Controller/Api/ApiController.php +++ b/src/Controller/Api/ApiController.php @@ -71,7 +71,8 @@ public function createPackageAction(Request $request): Response $form = $this->createForm($formType, $package, [ 'csrf_protection' => false, 'validation_groups' => ['Create', 'Default'], - 'is_created' => true + 'is_created' => true, + 'repo_type' => RepTypes::normalizeType($type), ]); $form->submit($payload); @@ -169,7 +170,8 @@ public function editPackageAction(Request $request, #[Vars] Package $package): R $form = $this->createForm($formType, $package, [ 'csrf_protection' => false, 'validation_groups' => ['Update', 'Default'], - 'is_created' => false + 'is_created' => false, + 'repo_type' => $package->getRepoType(), ]); $form->submit($payload, false); diff --git a/src/Controller/PackageController.php b/src/Controller/PackageController.php index 0f6aec44..c6f12d11 100644 --- a/src/Controller/PackageController.php +++ b/src/Controller/PackageController.php @@ -122,6 +122,7 @@ public function submitPackageAction(Request $req, string $type = null): Response 'action' => $this->generateUrl('submit', ['type' => $type]), 'validation_groups' => ['Create', 'Default'], 'is_created' => true, + 'repo_type' => RepTypes::normalizeType($type), ]); if ($this->getUser() instanceof User) { @@ -166,6 +167,7 @@ public function fetchInfoAction(Request $req, string $type = null): Response [ 'validation_groups' => ['Create', 'Default'], 'is_created' => true, + 'repo_type' => RepTypes::normalizeType($type), ] ); @@ -233,7 +235,8 @@ public function fetchMonoRepoInfo(Request $req): Response [ 'validation_groups' => ['Update'], 'is_created' => false, - 'allow_extra_fields' => true + 'allow_extra_fields' => true, + 'repo_type' => RepTypes::MONO_REPO, ] ); @@ -813,6 +816,7 @@ public function editAction(Request $req, #[Vars] Package $package): Response $form = $this->createForm($formTypeClass, $package, [ 'action' => $this->generateUrl('edit_package', ['name' => $package->getName()]), 'validation_groups' => ['Update', 'Default'], + 'repo_type' => $package->getRepoType(), ]); $form->handleRequest($req); diff --git a/src/Entity/Package.php b/src/Entity/Package.php index 7d630c61..0f75b5c5 100644 --- a/src/Entity/Package.php +++ b/src/Entity/Package.php @@ -397,7 +397,7 @@ public function setRepositoryPath(?string $path): void $this->repository = $path; } - if ($this->getRepoType() === RepTypes::CUSTOM) { + if ($this->getRepoType() === RepTypes::CUSTOM || $this->getRepoType() === RepTypes::VIRTUAL) { $this->customDriver = $this->driverError = null; $this->repository = $path; } diff --git a/src/Form/Type/Package/BasePackageType.php b/src/Form/Type/Package/BasePackageType.php index b2419a31..997b48a0 100644 --- a/src/Form/Type/Package/BasePackageType.php +++ b/src/Form/Type/Package/BasePackageType.php @@ -27,7 +27,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'MonoRepos (only GIT)' => RepTypes::MONO_REPO, 'Artifacts' => RepTypes::ARTIFACT, 'Custom (JSON)' => RepTypes::CUSTOM, - 'Satis / Packagist.com / VCS Import' => 'import' + 'Virtual (only JSON metadata)' => RepTypes::VIRTUAL, + 'Satis / Packagist.com / VCS Import' => 'import', // only redirect ]; if ($options['has_active_integration']) { @@ -52,6 +53,7 @@ protected function hasActiveIntegration(): bool public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('is_created', false); + $resolver->setDefault('repo_type', null); $resolver->setDefault('data_class', Package::class); $resolver->setDefault('has_active_integration', $this->hasActiveIntegration()); } diff --git a/src/Form/Type/Package/CustomPackageType.php b/src/Form/Type/Package/CustomPackageType.php index 50364896..86023424 100644 --- a/src/Form/Type/Package/CustomPackageType.php +++ b/src/Form/Type/Package/CustomPackageType.php @@ -5,6 +5,7 @@ use Doctrine\Persistence\ManagerRegistry; use Packeton\Form\Handler\CustomPackageHandler; use Packeton\Form\Type\EmbedCollectionType; +use Packeton\Package\RepTypes; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; @@ -38,7 +39,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'allow_add' => true, 'allow_delete' => true, 'entry_options' => [ - 'dist_choices' => $this->getChoices($options['is_created']) + 'dist_choices' => $this->getChoices($options['is_created']), + 'with_dist' => $options['repo_type'] !== RepTypes::VIRTUAL, ], ]); diff --git a/src/Form/Type/Package/CustomVersionType.php b/src/Form/Type/Package/CustomVersionType.php index 95b045c5..a2b783da 100644 --- a/src/Form/Type/Package/CustomVersionType.php +++ b/src/Form/Type/Package/CustomVersionType.php @@ -9,6 +9,7 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\NotBlank; @@ -29,13 +30,19 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $builder ->add('version', TextType::class, [ 'constraints' => [new NotBlank()] - ]) - ->add('dist', ChoiceType::class, [ - 'required' => false, - 'label' => 'Uploaded dist', - 'choices' => $options['dist_choices'], - 'attr' => ['class' => 'jselect2 archive-select'] - ]) + ]); + + if ($options['with_dist']) { + $builder + ->add('dist', ChoiceType::class, [ + 'required' => false, + 'label' => 'Uploaded dist', + 'choices' => $options['dist_choices'], + 'attr' => ['class' => 'jselect2 archive-select'] + ]); + } + + $builder ->add('definition', JsonTextType::class, [ 'required' => false, 'label' => 'composer.json config', @@ -50,8 +57,15 @@ public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'dist_choices' => null, - 'constraints' => [new Callback($this->validateData(...))] + 'with_dist' => true, ]); + + $resolver->addNormalizer('constraints', function(Options $options): array { + if ($options['with_dist']) { + return [new Callback($this->validateData(...))]; + } + return []; + }); } public function validateData($value, ExecutionContextInterface $context): void diff --git a/src/Model/VirtualPackageManager.php b/src/Model/VirtualPackageManager.php new file mode 100644 index 00000000..953f2ff5 --- /dev/null +++ b/src/Model/VirtualPackageManager.php @@ -0,0 +1,54 @@ +getVersionByReference($reference) ?: $package->getVersions()->first(); + if (null === $version) { + throw new \RuntimeException("VirtualPackage. Not found any versions for reference '$reference' of package '{$package->getName()}'"); + } + + $keyName = $this->config->buildName($package->getName(), $version->getReference(), $version->getVersion()); + $cachedName = $this->config->resolvePath($keyName); + if (file_exists($cachedName)) { + return $cachedName; + } + + $selected = []; + $serialized = $package->getCustomVersions(); + foreach ($serialized as $data) { + $verName = $data['version'] ?? null; + if ($verName === $version->getVersion() || $verName === $version->getNormalizedVersion()) { + $selected = $data['definition'] ?? []; + $selected['version'] = $version->getVersion(); + } + } + + $selected['name'] = $package->getName(); + $dir = dirname($cachedName); + if (!is_dir($dir)) { + @mkdir($dir, 0777, true); + } + + $zip = new \ZipArchive(); + $zip->open($cachedName, \ZipArchive::CREATE); + $zip->addFromString('composer.json', json_encode($selected, \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_PRETTY_PRINT)); + $zip->close(); + + return $cachedName; + } + +} diff --git a/src/Package/RepTypes.php b/src/Package/RepTypes.php index 80c2bc8a..de536f51 100644 --- a/src/Package/RepTypes.php +++ b/src/Package/RepTypes.php @@ -17,6 +17,7 @@ class RepTypes public const ARTIFACT = 'artifact'; public const INTEGRATION = 'integration'; public const CUSTOM = 'custom'; + public const VIRTUAL = 'virtual'; private static $types = [ self::ARTIFACT, @@ -24,6 +25,7 @@ class RepTypes self::INTEGRATION, self::VCS, self::CUSTOM, + self::VIRTUAL, ]; public static function getFormType(?string $type): string @@ -32,15 +34,20 @@ public static function getFormType(?string $type): string self::MONO_REPO => MonoRepoPackageType::class, self::ARTIFACT => ArtifactPackageType::class, self::INTEGRATION => IntegrationPackageType::class, - self::CUSTOM => CustomPackageType::class, + self::CUSTOM, self::VIRTUAL => CustomPackageType::class, default => PackageType::class, }; } + public static function isNotAutoCrawled(): array + { + return [self::VIRTUAL, self::CUSTOM, self::ARTIFACT]; + } + public static function isBuildInDist(?string $type): bool { return match ($type) { - self::ARTIFACT, self::CUSTOM => true, + self::ARTIFACT, self::CUSTOM, self::VIRTUAL => true, default => false, }; } @@ -49,7 +56,7 @@ public static function getUITemplate(?string $type, string $action): ?string { return match ($type) { self::ARTIFACT => "package/{$action}Artifact.html.twig", - self::CUSTOM => "package/{$action}Custom.html.twig", + self::CUSTOM, self::VIRTUAL => "package/{$action}Custom.html.twig", default => null, }; } @@ -61,6 +68,7 @@ public static function normalizeType(?string $type): string self::ARTIFACT => self::ARTIFACT, self::INTEGRATION => self::INTEGRATION, self::CUSTOM => self::CUSTOM, + self::VIRTUAL => self::VIRTUAL, default => self::VCS, }; } diff --git a/src/Package/Updater.php b/src/Package/Updater.php index 321bc75c..481d6db8 100644 --- a/src/Package/Updater.php +++ b/src/Package/Updater.php @@ -102,7 +102,7 @@ public function __construct( */ public static function supportRepoTypes(): iterable { - return [RepTypes::VCS, RepTypes::ARTIFACT, RepTypes::INTEGRATION, RepTypes::CUSTOM]; + return [RepTypes::VCS, RepTypes::ARTIFACT, RepTypes::INTEGRATION, RepTypes::CUSTOM, RepTypes::VIRTUAL]; } /** @@ -556,7 +556,7 @@ private function updateArchive(PackageInterface $data, Package $package): ?array // Process local path repos if (is_string($distUrl = $data->getDistUrl()) && (str_starts_with($distUrl, '/') || $distUrl === DistConfig::HOSTNAME_PLACEHOLDER) - && (empty($data->getSourceUrl()) || $package->getRepoType() === RepTypes::CUSTOM) + && (empty($data->getSourceUrl()) || in_array($package->getRepoType(), [RepTypes::CUSTOM, RepTypes::VIRTUAL], true)) ) { return [ 'url' => $this->distConfig->generateRoute($data->getName(), $data->getDistReference(), $data->getDistType()), diff --git a/src/Repository/PackageRepository.php b/src/Repository/PackageRepository.php index 6eadd047..b8a221d5 100644 --- a/src/Repository/PackageRepository.php +++ b/src/Repository/PackageRepository.php @@ -21,6 +21,7 @@ use Packeton\Entity\Package; use Packeton\Entity\User; use Packeton\Entity\Version; +use Packeton\Package\RepTypes; use Packeton\Service\SubRepositoryHelper; use Packeton\Util\PacketonUtils; @@ -219,7 +220,7 @@ public function getStalePackages($interval = null) "SELECT p.id FROM package p WHERE p.abandoned = false AND p.parent_id is NULL - AND (p.repo_type NOT IN ('artifact', 'custom') OR p.repo_type IS NULL) + AND (p.repo_type NOT IN (:notcrawled) OR p.repo_type IS NULL) AND ( p.crawledAt IS NULL OR (p.autoUpdated = false AND p.crawledAt < :crawled) @@ -231,6 +232,10 @@ public function getStalePackages($interval = null) 'crawled' => date('Y-m-d H:i:s', time() - ($interval ?: 14400)), // crawl auto-updated packages once a week just in case 'autocrawled' => date('Y-m-d H:i:s', strtotime('-7day')), + 'notcrawled' => RepTypes::isNotAutoCrawled() ?: ['na'], + ], + [ + 'notcrawled' => ArrayParameterType::STRING ] ); } diff --git a/src/Service/DistManager.php b/src/Service/DistManager.php index f92aa407..79640f0d 100644 --- a/src/Service/DistManager.php +++ b/src/Service/DistManager.php @@ -19,6 +19,7 @@ use Packeton\Integrations\IntegrationRegistry; use Packeton\Integrations\ZipballInterface; use Packeton\Model\UploadZipballStorage; +use Packeton\Model\VirtualPackageManager; use Packeton\Package\RepTypes; use Symfony\Component\Filesystem\Filesystem; @@ -34,6 +35,7 @@ public function __construct( private readonly IntegrationRegistry $integrations, private readonly FilesystemOperator $baseStorage, private readonly Filesystem $fs, + private readonly VirtualPackageManager $virtualPackageManager, ) { } @@ -169,17 +171,21 @@ private function guessCompletePackage(string $reference, array $versions): ?Comp private function downloadArtifact(string $reference, Package $package): ?string { + if ($package->getRepoType() === RepTypes::VIRTUAL) { + return $this->virtualPackageManager->buildArchive($package, $reference); + } + if ($path = $this->artifact->moveToLocal($reference)) { return $path; } $repository = $this->createRepositoryAndIo($package); $packages = $repository->getPackages(); - $found = array_filter($packages, fn($p) => $reference === $p->getDistReference()); + $found = array_filter($packages, static fn($p) => $reference === $p->getDistReference()); - /** @var PackageInterface $package */ - if ($package = reset($found)) { - $distUrl = $package->getDistUrl(); + /** @var PackageInterface $pkg */ + if ($pkg = reset($found)) { + $distUrl = $pkg->getDistUrl(); if (is_string($distUrl) && str_starts_with($distUrl, '/')) { return $distUrl; } diff --git a/src/Validator/Constraint/PackageRepositoryValidator.php b/src/Validator/Constraint/PackageRepositoryValidator.php index 8082f197..32f663d6 100644 --- a/src/Validator/Constraint/PackageRepositoryValidator.php +++ b/src/Validator/Constraint/PackageRepositoryValidator.php @@ -34,7 +34,7 @@ public function validate(mixed $value, Constraint $constraint): void match ($value->getRepoType()) { RepTypes::ARTIFACT => $this->validateArtifactPackage($value), - RepTypes::CUSTOM => $this->validateCustomPackage($value), + RepTypes::CUSTOM, RepTypes::VIRTUAL => $this->validateCustomPackage($value), default => $this->validateVcsPackage($value), }; }