diff --git a/app/Http/Controllers/Composer/DistController.php b/app/Http/Controllers/Composer/DistController.php index c930cb37..c39959b4 100644 --- a/app/Http/Controllers/Composer/DistController.php +++ b/app/Http/Controllers/Composer/DistController.php @@ -21,15 +21,17 @@ public function download( ): Response { $packageName = "{$vendor}/{$package}"; - $packageVersion = PackageVersion::query() + $packageVersions = PackageVersion::query() ->whereHas('package', function ($query) use ($organization, $packageName) { $query->where('organization_uuid', $organization->uuid) ->where('name', $packageName); }) - ->where('version', $version) + ->matchingVersion($version) ->where('source_reference', $reference) ->whereNotNull('dist_path') - ->first(); + ->get(); + + $packageVersion = $packageVersions->firstWhere('version', $version) ?? $packageVersions->first(); if (! $packageVersion || ! $packageVersion->dist_path) { return response()->json(['error' => 'Not found'], 404); diff --git a/app/Models/PackageVersion.php b/app/Models/PackageVersion.php index d350de8b..7b03a290 100644 --- a/app/Models/PackageVersion.php +++ b/app/Models/PackageVersion.php @@ -108,6 +108,24 @@ public function scopeStable(Builder $query): Builder ->whereNotLike('normalized_version', '%-dev'); } + /** + * Lock files created before versions kept their v prefix reference dist + * URLs without it, so match the version with and without the prefix. + * + * @param Builder $query + * @return Builder + */ + public function scopeMatchingVersion(Builder $query, string $version): Builder + { + $candidates = match (true) { + preg_match('/^\d/', $version) === 1 => [$version, "v{$version}"], + preg_match('/^v\d/', $version) === 1 => [$version, substr($version, 1)], + default => [$version], + }; + + return $query->whereIn('version', $candidates); + } + /** * @param Builder $query * @return Builder diff --git a/tests/Feature/Composer/DistDownloadTest.php b/tests/Feature/Composer/DistDownloadTest.php index 318445e1..22e0171c 100644 --- a/tests/Feature/Composer/DistDownloadTest.php +++ b/tests/Feature/Composer/DistDownloadTest.php @@ -130,6 +130,75 @@ function distGet(string $uri, string $token): TestResponse $response->assertOk(); }); +it('downloads a dist archive when requesting a v-prefixed version without the prefix', function () { + $package = Package::factory() + ->for($this->organization, 'organization') + ->create(['name' => 'acme/test-package']); + + $distPath = 'acme/acme/test-package/v1.2.0_abc123def456.zip'; + Storage::disk('local')->put($distPath, 'fake-zip-content'); + + PackageVersion::factory() + ->for($package) + ->create([ + 'version' => 'v1.2.0', + 'source_reference' => 'abc123def456', + 'dist_url' => url('/acme/dists/acme/test-package/v1.2.0/abc123def456.zip'), + 'dist_path' => $distPath, + 'dist_shasum' => sha1('fake-zip-content'), + ]); + + $response = distGet('/acme/dists/acme/test-package/1.2.0/abc123def456.zip', $this->plainToken); + + $response->assertOk(); +}); + +it('downloads a dist archive when requesting an unprefixed version with a v prefix', function () { + $package = Package::factory() + ->for($this->organization, 'organization') + ->create(['name' => 'acme/test-package']); + + $distPath = 'acme/acme/test-package/1.2.0_abc123def456.zip'; + Storage::disk('local')->put($distPath, 'fake-zip-content'); + + PackageVersion::factory() + ->for($package) + ->create([ + 'version' => '1.2.0', + 'source_reference' => 'abc123def456', + 'dist_url' => url('/acme/dists/acme/test-package/1.2.0/abc123def456.zip'), + 'dist_path' => $distPath, + 'dist_shasum' => sha1('fake-zip-content'), + ]); + + $response = distGet('/acme/dists/acme/test-package/v1.2.0/abc123def456.zip', $this->plainToken); + + $response->assertOk(); +}); + +it('returns 404 for a legacy version request with a non-matching reference', function () { + $package = Package::factory() + ->for($this->organization, 'organization') + ->create(['name' => 'acme/test-package']); + + $distPath = 'acme/acme/test-package/v1.2.0_abc123def456.zip'; + Storage::disk('local')->put($distPath, 'fake-zip-content'); + + PackageVersion::factory() + ->for($package) + ->create([ + 'version' => 'v1.2.0', + 'source_reference' => 'abc123def456', + 'dist_url' => url('/acme/dists/acme/test-package/v1.2.0/abc123def456.zip'), + 'dist_path' => $distPath, + 'dist_shasum' => sha1('fake-zip-content'), + ]); + + $response = distGet('/acme/dists/acme/test-package/1.2.0/0123456789ab.zip', $this->plainToken); + + $response->assertNotFound(); +}); + it('requires authentication for dist download', function () { $response = test()->getJson('/acme/dists/acme/test-package/1.0.0/abc123.zip');