Skip to content

Commit 33d5a5d

Browse files
committed
Save local distributions to the database
1 parent a81e1f5 commit 33d5a5d

11 files changed

Lines changed: 336 additions & 33 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
* 0.8.0
4+
* **Breaking changes**
5+
* The path for stored distributions has changed, so any previously downloaded distributions are no longer valid.
6+
It's recommended to delete the existing `distribution/` directory in the data directory before upgrading to
7+
this version to prevent any lingering distributions taking up storage space.
8+
39
* 0.7.2 (2026-07-01)
410
* Fixed issue with package links not being updated
511
* Updated `guzzlehttp/psr7` dependency to fix potential security vulnerability

config/packages/security.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ security:
1919
pattern: ^/(_profiler|_wdt|assets|build)/
2020
security: false
2121
api:
22-
pattern: ^/(packages.json|downloads|p2/|dist/)
22+
pattern: ^/(packages.json|downloads|p2/|dist/|dist-mirror/)
2323
provider: database_users
2424
http_basic:
2525
realm: api
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
final class Version20260708083050 extends AbstractMigration
11+
{
12+
public function getDescription(): string
13+
{
14+
return 'Create distribution table';
15+
}
16+
17+
public function up(Schema $schema): void
18+
{
19+
$this->addSql(<<<'SQL'
20+
CREATE TABLE distribution (
21+
id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
22+
reference VARCHAR(255) NOT NULL,
23+
type VARCHAR(255) NOT NULL,
24+
sha1_checksum VARCHAR(255) DEFAULT NULL,
25+
source TEXT DEFAULT NULL,
26+
resolved_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
27+
created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
28+
last_modified_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,
29+
metadata_id INT NOT NULL,
30+
PRIMARY KEY (id)
31+
)
32+
SQL);
33+
$this->addSql(<<<'SQL'
34+
CREATE INDEX IDX_A4483781DC9EE959 ON distribution (metadata_id)
35+
SQL);
36+
$this->addSql(<<<'SQL'
37+
CREATE UNIQUE INDEX UNIQ_A4483781DC9EE959AEA349138CDE5729 ON distribution (metadata_id, reference, type)
38+
SQL);
39+
$this->addSql(<<<'SQL'
40+
ALTER TABLE
41+
distribution
42+
ADD
43+
CONSTRAINT FK_A4483781DC9EE959 FOREIGN KEY (metadata_id) REFERENCES metadata (id) NOT DEFERRABLE
44+
SQL);
45+
}
46+
47+
public function down(Schema $schema): void
48+
{
49+
$this->addSql(<<<'SQL'
50+
ALTER TABLE distribution DROP CONSTRAINT FK_A4483781DC9EE959
51+
SQL);
52+
$this->addSql(<<<'SQL'
53+
DROP TABLE distribution
54+
SQL);
55+
}
56+
}

src/Controller/ApiController.php

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ public function __construct(
4141
private readonly PackageDistributionResolver $distributionResolver,
4242
private readonly PackageProviderManager $providerManager,
4343
private readonly MessageBusInterface $messenger,
44+
#[Autowire(param: 'dirigent.distributions.async_api_requests')]
45+
private readonly bool $asyncApiRequests,
4446
#[Autowire(param: 'dirigent.packages.dynamic_updates')]
4547
private readonly bool $dynamicUpdatesEnabled,
4648
#[Autowire(param: 'dirigent.metadata.mirror_vcs_repositories')]
47-
private readonly bool $mirrorVcsRepositories = false,
49+
private readonly bool $mirrorVcsRepositories,
4850
) {
4951
}
5052

@@ -63,7 +65,7 @@ public function root(RouterInterface $router): JsonResponse
6365
];
6466

6567
if ($this->getParameter('dirigent.distributions.mirror')) {
66-
$distributionUrlPattern = u($router->getRouteCollection()->get('api_package_distribution')->getPath())
68+
$distributionUrlPattern = u($router->getRouteCollection()->get('api_package_distribution_mirror')->getPath())
6769
->replace('{package}', '%package%')
6870
->replace('{version}', '%version%')
6971
->replace('{reference}', '%reference%')
@@ -105,18 +107,19 @@ public function packageMetadata(Request $request): Response
105107
return $response;
106108
}
107109

108-
#[Route('/dist/{package}/{version}-{reference}.{type}',
110+
#[Route('/dist/{package}/{version}-r{revision}-{reference}.{type}',
109111
name: 'api_package_distribution',
110112
requirements: [
111113
'package' => MapPackage::PACKAGE_REGEX,
112114
'version' => '.+',
115+
'revision' => '[1-9][0-9]*',
113116
'reference' => '[a-z0-9]+',
114117
'type' => '(zip)',
115118
],
116119
methods: ['GET'],
117120
)]
118121
#[IsGrantedAccess]
119-
public function packageDistribution(Request $request, string $reference, string $type): Response
122+
public function packageDistribution(Request $request, int $revision, string $reference, string $type): Response
120123
{
121124
if (!$this->getParameter('dirigent.distributions.enabled')) {
122125
throw $this->createNotFoundException();
@@ -125,21 +128,57 @@ public function packageDistribution(Request $request, string $reference, string
125128
$packageName = $request->attributes->get('package');
126129
$versionName = $request->attributes->get('version');
127130

128-
if (!$this->distributionResolver->exists($packageName, $versionName, $reference, $type)) {
129-
if (null === $package = $this->findPackage($packageName)) {
130-
throw $this->createNotFoundException();
131-
}
131+
if (null === $package = $this->findPackage($packageName)) {
132+
throw $this->createNotFoundException();
133+
}
132134

133-
if (null === $metadata = $this->metadataRepository->findOneByNormalizedNameAndReference($package, $versionName, $reference)) {
134-
throw $this->createNotFoundException();
135-
}
135+
if (null === $metadata = $this->metadataRepository->findOneByNormalizedNameAndRevision($package, $versionName, $revision)) {
136+
throw $this->createNotFoundException();
137+
}
136138

137-
if (!$this->distributionResolver->resolve($metadata, $type, async: $this->getParameter('dirigent.distributions.async_api_requests'))) {
138-
throw $this->createNotFoundException();
139-
}
139+
if (!$this->distributionResolver->resolve($metadata, $reference, $type, async: $this->asyncApiRequests)) {
140+
throw $this->createNotFoundException();
141+
}
142+
143+
$path = $this->distributionResolver->path($metadata, $reference, $type);
144+
$filename = u("$packageName-$versionName-r$revision-$reference.$type")->replace('/', '-')->toString();
145+
146+
return $this->file($path, $filename);
147+
}
148+
149+
#[Route('/dist-mirror/{package}/{version}-{reference}.{type}',
150+
name: 'api_package_distribution_mirror',
151+
requirements: [
152+
'package' => MapPackage::PACKAGE_REGEX,
153+
'version' => '.+',
154+
'reference' => '[a-z0-9]+',
155+
'type' => '(zip)',
156+
],
157+
methods: ['GET'],
158+
)]
159+
#[IsGrantedAccess]
160+
public function packageDistributionMirror(Request $request, string $reference, string $type): Response
161+
{
162+
if (!$this->getParameter('dirigent.distributions.enabled')) {
163+
throw $this->createNotFoundException();
164+
}
165+
166+
$packageName = $request->attributes->get('package');
167+
$versionName = $request->attributes->get('version');
168+
169+
if (null === $package = $this->findPackage($packageName)) {
170+
throw $this->createNotFoundException();
171+
}
172+
173+
if (null === $metadata = $this->metadataRepository->findOneByNormalizedNameAndDistributionReference($package, $versionName, $reference)) {
174+
throw $this->createNotFoundException();
175+
}
176+
177+
if (!$this->distributionResolver->resolve($metadata, $reference, $type, async: $this->asyncApiRequests)) {
178+
throw $this->createNotFoundException();
140179
}
141180

142-
$path = $this->distributionResolver->path($packageName, $versionName, $reference, $type);
181+
$path = $this->distributionResolver->path($metadata, $reference, $type);
143182
$filename = u("$packageName-$versionName-$reference.$type")->replace('/', '-')->toString();
144183

145184
return $this->file($path, $filename);
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CodedMonkey\Dirigent\Doctrine\Entity;
6+
7+
use CodedMonkey\Dirigent\Doctrine\Repository\DistributionRepository;
8+
use Doctrine\DBAL\Types\Types;
9+
use Doctrine\ORM\Mapping as ORM;
10+
11+
#[ORM\Entity(repositoryClass: DistributionRepository::class)]
12+
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
13+
#[ORM\UniqueConstraint(columns: ['metadata_id', 'reference', 'type'])]
14+
class Distribution extends TrackedEntity
15+
{
16+
#[ORM\Column, ORM\GeneratedValue, ORM\Id]
17+
private ?int $id = null;
18+
19+
#[ORM\Column]
20+
private string $reference;
21+
22+
#[ORM\Column]
23+
private string $type;
24+
25+
#[ORM\Column(nullable: true)]
26+
private ?string $sha1Checksum = null;
27+
28+
/**
29+
* Source URL.
30+
*
31+
* Contains the source URL if the distribution is mirrored.
32+
*/
33+
#[ORM\Column(type: Types::TEXT, nullable: true)]
34+
private ?string $source = null;
35+
36+
#[ORM\Column]
37+
private ?\DateTimeImmutable $resolvedAt = null;
38+
39+
#[ORM\ManyToOne(inversedBy: 'distributions')]
40+
#[ORM\JoinColumn(nullable: false)]
41+
private Metadata $metadata;
42+
43+
public function __construct(Metadata $metadata, string $reference, string $type)
44+
{
45+
$this->metadata = $metadata;
46+
$this->reference = $reference;
47+
$this->type = $type;
48+
}
49+
50+
public function getId(): ?int
51+
{
52+
return $this->id;
53+
}
54+
55+
public function getReference(): string
56+
{
57+
return $this->reference;
58+
}
59+
60+
public function getType(): string
61+
{
62+
return $this->type;
63+
}
64+
65+
public function getSha1Checksum(): ?string
66+
{
67+
return $this->sha1Checksum;
68+
}
69+
70+
public function setSha1Checksum(?string $sha1Checksum): void
71+
{
72+
$this->sha1Checksum = $sha1Checksum;
73+
}
74+
75+
public function getSource(): ?string
76+
{
77+
return $this->source;
78+
}
79+
80+
public function setSource(?string $source): void
81+
{
82+
$this->source = $source;
83+
}
84+
85+
public function getResolvedAt(): ?\DateTimeImmutable
86+
{
87+
return $this->resolvedAt;
88+
}
89+
90+
public function setResolvedAt(): void
91+
{
92+
$this->resolvedAt = new \DateTimeImmutable();
93+
}
94+
95+
public function getMetadata(): Metadata
96+
{
97+
return $this->metadata;
98+
}
99+
}

src/Doctrine/Entity/Metadata.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ class Metadata extends TrackedEntity implements \Stringable
119119
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
120120
private MetadataFiles $files;
121121

122+
#[ORM\OneToMany(targetEntity: Distribution::class, mappedBy: 'metadata')]
123+
private Collection $distributions;
124+
122125
/**
123126
* @var Collection<int, MetadataRequireLink>&Selectable
124127
*/
@@ -167,6 +170,7 @@ public function __construct(Version $version)
167170
$this->package = $version->getPackage();
168171
$this->files = new MetadataFiles($this);
169172

173+
$this->distributions = new ArrayCollection();
170174
$this->requireLinks = new ArrayCollection();
171175
$this->devRequireLinks = new ArrayCollection();
172176
$this->conflictLinks = new ArrayCollection();
@@ -415,6 +419,14 @@ public function getPackage(): Package
415419
return $this->package;
416420
}
417421

422+
/**
423+
* @return Collection<int, Distribution>
424+
*/
425+
public function getDistributions(): Collection
426+
{
427+
return $this->distributions;
428+
}
429+
418430
/**
419431
* @return Collection<int, MetadataRequireLink>
420432
*/
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CodedMonkey\Dirigent\Doctrine\Repository;
6+
7+
use CodedMonkey\Dirigent\Doctrine\Entity\Distribution;
8+
use CodedMonkey\Dirigent\Doctrine\Entity\Metadata;
9+
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
10+
use Doctrine\Persistence\ManagerRegistry;
11+
12+
/**
13+
* @extends ServiceEntityRepository<Distribution>
14+
*
15+
* @method Distribution|null find($id, $lockMode = null, $lockVersion = null)
16+
* @method Distribution[] findAll()
17+
* @method Distribution[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
18+
* @method Distribution|null findOneBy(array $criteria, array $orderBy = null)
19+
*/
20+
class DistributionRepository extends ServiceEntityRepository
21+
{
22+
public function __construct(ManagerRegistry $registry)
23+
{
24+
parent::__construct($registry, Distribution::class);
25+
}
26+
27+
public function save(Distribution $entity, bool $flush = false): void
28+
{
29+
$this->getEntityManager()->persist($entity);
30+
31+
if ($flush) {
32+
$this->getEntityManager()->flush();
33+
}
34+
}
35+
36+
public function remove(Distribution $entity, bool $flush = false): void
37+
{
38+
$this->getEntityManager()->remove($entity);
39+
40+
if ($flush) {
41+
$this->getEntityManager()->flush();
42+
}
43+
}
44+
45+
public function findOneByReferenceAndType(Metadata $metadata, string $reference, string $type): ?Distribution
46+
{
47+
return $this->findOneBy([
48+
'metadata' => $metadata,
49+
'reference' => $reference,
50+
'type' => $type,
51+
]);
52+
}
53+
}

0 commit comments

Comments
 (0)