[web-downloads] main: Add endpoint to list builds

From: Date: Mon, 10 Nov 2025 06:07:10 +0000
Subject: [web-downloads] main: Add endpoint to list builds
Groups: php.webmaster 
Request: Send a blank email to php-webmaster+get-33330@lists.php.net to get a copy of this message
Author: Shivam Mathur (shivammathur) Date: 2025-11-10T11:37:03+05:30 Commit: https://github.com/php/web-downloads/commit/daab2fa752eadce4f936b52577c2d972a728b4df Raw diff: https://github.com/php/web-downloads/commit/daab2fa752eadce4f936b52577c2d972a728b4df.diff Add endpoint to list builds Changed paths: A src/Http/Controllers/ListBuildsController.php A tests/Http/Controllers/ListBuildsControllerTest.php M API.md M routes.php Diff: diff --git a/API.md b/API.md index 7056060..46917be 100644 --- a/API.md +++ b/API.md @@ -25,6 +25,26 @@ --- +### GET /api/list-builds + +- Auth: Required +- Purpose: Enumerate the files under BUILDS_DIRECTORY so operators can inspect available build artifacts. +- Request body: none (GET request). +- Success: 200 OK with JSON payload { "builds": [ { "path": "relative/path", "size": 1234, "modified": "2025-09-30T12:34:56+00:00" }, ... ] }. +- Errors: + - 401 if the bearer token is missing or invalid. + - 500 with { "error": "Builds directory not configured or missing." } if BUILDS_DIRECTORY is unset or the directory does not exist. + +Example + +```bash +curl -i -X GET \ + -H "Authorization: Bearer $AUTH_TOKEN" \ + https://downloads.php.net/api/list-builds +``` + +--- + ### POST /api/php - Auth: Required diff --git a/routes.php b/routes.php index bdf6001..9b707a4 100644 --- a/routes.php +++ b/routes.php @@ -2,6 +2,7 @@ declare(strict_types=1); use App\Http\Controllers\IndexController; +use App\Http\Controllers\ListBuildsController; use App\Http\Controllers\PeclController; use App\Http\Controllers\PhpController; use App\Http\Controllers\SeriesDeleteController; @@ -12,6 +13,7 @@ $router = new Router(); $router->registerRoute('/api', 'GET', IndexController::class); +$router->registerRoute('/api/list-builds', 'GET', ListBuildsController::class, true); $router->registerRoute('/api/pecl', 'POST', PeclController::class, true); $router->registerRoute('/api/winlibs', 'POST', WinlibsController::class, true); $router->registerRoute('/api/php', 'POST', PhpController::class, true); diff --git a/src/Http/Controllers/ListBuildsController.php b/src/Http/Controllers/ListBuildsController.php new file mode 100644 index 0000000..3be3732 --- /dev/null +++ b/src/Http/Controllers/ListBuildsController.php @@ -0,0 +1,79 @@ +<?php +declare(strict_types=1); + +namespace App\Http\Controllers; + +use App\Http\ControllerInterface; +use FilesystemIterator; +use JsonException; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; + +class ListBuildsController implements ControllerInterface +{ + public function __construct(private ?string $buildsDirectory = null) + { + if ($this->buildsDirectory === null) { + $this->buildsDirectory = getenv('BUILDS_DIRECTORY') ?: ''; + } + } + + public function handle(): void + { + if ($this->buildsDirectory === '' || !is_dir($this->buildsDirectory)) { + http_response_code(500); + $this->outputJson(['error' => 'Builds directory not configured or missing.']); + return; + } + + $builds = $this->collectBuilds($this->buildsDirectory); + + http_response_code(200); + $this->outputJson(['builds' => $builds]); + } + + private function collectBuilds(string $root): array + { + $entries = []; + + $normalizedRoot = rtrim($root, DIRECTORY_SEPARATOR); + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($normalizedRoot, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $fileInfo) { + if (!$fileInfo->isFile()) { + continue; + } + + $relativePath = substr($fileInfo->getPathname(), strlen($normalizedRoot) + 1); + + $entries[] = [ + 'path' => $relativePath, + 'size' => $fileInfo->getSize(), + 'modified' => gmdate('c', $fileInfo->getMTime()), + ]; + } + + usort($entries, static fn (array $a, array $b): int => strcmp($a['path'], $b['path'])); + + return $entries; + } + + private function outputJson(array $payload): void + { + try { + $json = json_encode($payload, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); + } catch (JsonException) { + http_response_code(500); + header('Content-Type: application/json'); + echo '{"error":"Failed to encode response."}'; + return; + } + + header('Content-Type: application/json'); + echo $json; + } +} diff --git a/tests/Http/Controllers/ListBuildsControllerTest.php b/tests/Http/Controllers/ListBuildsControllerTest.php new file mode 100644 index 0000000..9b4077e --- /dev/null +++ b/tests/Http/Controllers/ListBuildsControllerTest.php @@ -0,0 +1,90 @@ +<?php +declare(strict_types=1); + +namespace Http\Controllers; + +use App\Http\Controllers\ListBuildsController; +use JsonException; +use PHPUnit\Framework\TestCase; + +class ListBuildsControllerTest extends TestCase +{ + public function testHandleOutputsBuildListing(): void + { + $tempDir = $this->createTempBuildDirectory(); + + $controller = new ListBuildsController($tempDir); + + http_response_code(200); + ob_start(); + $controller->handle(); + $output = ob_get_clean(); + + static::assertNotFalse($output); + + $data = $this->decodeJson($output); + + static::assertSame(200, http_response_code()); + static::assertArrayHasKey('builds', $data); + static::assertCount(2, $data['builds']); + static::assertSame('php/build-one.zip', $data['builds'][0]['path']); + static::assertSame('winlibs/run/info.json', $data['builds'][1]['path']); + + $this->removeDirectory($tempDir); + } + + public function testHandleReturnsErrorWhenDirectoryMissing(): void + { + $controller = new ListBuildsController('/path/to/missing/builds'); + + http_response_code(200); + ob_start(); + $controller->handle(); + $output = ob_get_clean(); + + $data = $this->decodeJson($output); + + static::assertSame(500, http_response_code()); + static::assertSame('Builds directory not configured or missing.', $data['error']); + } + + private function decodeJson(string $json): array + { + try { + return json_decode($json, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + static::fail('Response is not valid JSON: ' . $exception->getMessage()); + } + + return []; + } + + private function createTempBuildDirectory(): string + { + $base = sys_get_temp_dir() . '/list-builds-' . uniqid(); + mkdir($base . '/php', 0755, true); + mkdir($base . '/winlibs/run', 0755, true); + + file_put_contents($base . '/php/build-one.zip', 'fake-zip-data'); + touch($base . '/php/build-one.zip', 1730000000); + + file_put_contents($base . '/winlibs/run/info.json', '{}'); + touch($base . '/winlibs/run/info.json', 1730003600); + + return $base; + } + + private function removeDirectory(string $path): void + { + $items = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($items as $item) { + $item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname()); + } + + rmdir($path); + } +}

« previous php.webmaster (#33330) next »