[web-downloads] main: Add api to delete pending jobs

From: Date: Mon, 10 Nov 2025 10:47:18 +0000
Subject: [web-downloads] main: Add api to delete pending jobs
Groups: php.webmaster 
Request: Send a blank email to php-webmaster+get-33331@lists.php.net to get a copy of this message
Author: Shivam Mathur (shivammathur) Date: 2025-11-10T16:17:09+05:30 Commit: https://github.com/php/web-downloads/commit/a5bc5e0171d3406cd86debb230ca388b6363cd52 Raw diff: https://github.com/php/web-downloads/commit/a5bc5e0171d3406cd86debb230ca388b6363cd52.diff Add api to delete pending jobs Changed paths: A src/Http/Controllers/DeletePendingJobController.php A tests/Http/Controllers/DeletePendingJobControllerTest.php M API.md M routes.php Diff: diff --git a/API.md b/API.md index 46917be..b5dacb2 100644 --- a/API.md +++ b/API.md @@ -28,7 +28,7 @@ ### GET /api/list-builds - Auth: Required -- Purpose: Enumerate the files under BUILDS_DIRECTORY so operators can inspect available build artifacts. +- Purpose: List builds artifacts pending for processing in the BUILDS_DIRECTORY. - 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: @@ -45,6 +45,34 @@ curl -i -X GET \ --- +### POST /api/delete-pending-job + +- Auth: Required +- Purpose: Remove a queued build job before it is processed. +- Request body (JSON): + - type (string, required): One of php, pecl, or winlibs. + - job (string, required): The job filename (for php/pecl) or directory name (for winlibs). +- Success: 200 OK with { "status": "deleted" }. +- Errors: + - 400 if validation fails (missing/invalid fields). + - 404 if the job could not be found. + - 500 if BUILDS_DIRECTORY is not configured or the delete operation fails. + +Example + +```bash +curl -i -X POST \ + -H "Authorization: Bearer $AUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "type": "php", + "job": "php-abc123.zip" + }' \ + https://downloads.php.net/api/delete-pending-job +``` + +--- + ### POST /api/php - Auth: Required diff --git a/routes.php b/routes.php index 9b707a4..46c959d 100644 --- a/routes.php +++ b/routes.php @@ -1,6 +1,7 @@ <?php declare(strict_types=1); +use App\Http\Controllers\DeletePendingJobController; use App\Http\Controllers\IndexController; use App\Http\Controllers\ListBuildsController; use App\Http\Controllers\PeclController; @@ -14,6 +15,7 @@ $router = new Router(); $router->registerRoute('/api', 'GET', IndexController::class); $router->registerRoute('/api/list-builds', 'GET', ListBuildsController::class, true); +$router->registerRoute('/api/delete-pending-job', 'POST', DeletePendingJobController::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/DeletePendingJobController.php b/src/Http/Controllers/DeletePendingJobController.php new file mode 100644 index 0000000..345b6e6 --- /dev/null +++ b/src/Http/Controllers/DeletePendingJobController.php @@ -0,0 +1,128 @@ +<?php +declare(strict_types=1); + +namespace App\Http\Controllers; + +use App\Helpers\Helpers; +use App\Http\BaseController; +use App\Validator; +use JsonException; +use RuntimeException; + +class DeletePendingJobController extends BaseController +{ + private string $buildsDirectory; + + public function __construct(string $inputPath = 'php://input', ?string $buildsDirectory = null) + { + parent::__construct($inputPath); + + $this->buildsDirectory = $buildsDirectory ?? (getenv('BUILDS_DIRECTORY') ?: ''); + } + + protected function validate(array $data): bool + { + $validator = new Validator([ + 'type' => 'required|string|regex:/^(php|pecl|winlibs)$/i', + 'job' => 'required|string|regex:/^[A-Za-z0-9._-]+$/', + ]); + + $validator->validate($data); + + $valid = $validator->isValid(); + + if (!$valid) { + http_response_code(400); + echo 'Invalid request: ' . $validator; + } + + return $valid; + } + + protected function execute(array $data): void + { + if ($this->buildsDirectory === '') { + http_response_code(500); + echo 'Error: Builds directory is not configured.'; + return; + } + + $type = strtolower($data['type']); + $jobName = $data['job']; + + try { + $this->deleteJob($type, $jobName); + http_response_code(200); + $this->outputJson(['status' => 'deleted']); + } catch (RuntimeException $runtimeException) { + $status = $runtimeException->getCode() ?: 500; + http_response_code($status); + echo 'Error: ' . $runtimeException->getMessage(); + } catch (JsonException) { + http_response_code(500); + echo 'Error: Failed to encode response.'; + } + } + + private function deleteJob(string $type, string $jobName): void + { + $path = $this->resolvePath($type, $jobName); + + if ($type === 'winlibs') { + $this->deleteDirectoryJob($path); + } else { + $this->deleteFileJob($path); + } + } + + private function resolvePath(string $type, string $jobName): string + { + return match ($type) { + 'php', 'pecl' => $this->buildsDirectory . '/' . $type . '/' . $jobName, + 'winlibs' => $this->buildsDirectory . '/winlibs/' . $jobName, + default => $this->buildsDirectory, + }; + } + + private function deleteFileJob(string $filePath): void + { + if (!is_file($filePath)) { + throw new RuntimeException('Job not found.', 404); + } + + if (!@unlink($filePath)) { + throw new RuntimeException('Unable to delete job file.', 500); + } + + $lockFile = $filePath . '.lock'; + if (is_file($lockFile)) { + @unlink($lockFile); + } + } + + private function deleteDirectoryJob(string $directoryPath): void + { + if (!is_dir($directoryPath)) { + throw new RuntimeException('Job not found.', 404); + } + + $helper = new Helpers(); + if (!$helper->rmdirr($directoryPath)) { + throw new RuntimeException('Unable to delete job directory.', 500); + } + + $lockFile = $directoryPath . '.lock'; + if (file_exists($lockFile)) { + @unlink($lockFile); + } + } + + /** + * @throws JsonException + */ + private function outputJson(array $payload): void + { + header('Content-Type: application/json'); + echo json_encode($payload, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); + } +} \ No newline at end of file diff --git a/tests/Http/Controllers/DeletePendingJobControllerTest.php b/tests/Http/Controllers/DeletePendingJobControllerTest.php new file mode 100644 index 0000000..c8694a3 --- /dev/null +++ b/tests/Http/Controllers/DeletePendingJobControllerTest.php @@ -0,0 +1,102 @@ +<?php +declare(strict_types=1); + +namespace Http\Controllers; + +use App\Helpers\Helpers; +use App\Http\Controllers\DeletePendingJobController; +use PHPUnit\Framework\TestCase; + +class DeletePendingJobControllerTest extends TestCase +{ + private string $tempDir; + + protected function setUp(): void + { + parent::setUp(); + + $this->tempDir = sys_get_temp_dir() . '/delete-pending-' . uniqid(); + mkdir($this->tempDir, 0755, true); + } + + protected function tearDown(): void + { + (new Helpers())->rmdirr($this->tempDir); + parent::tearDown(); + } + + public function testDeletesPhpJobAndLock(): void + { + $phpDir = $this->tempDir . '/php'; + mkdir($phpDir, 0755, true); + $jobFile = $phpDir . '/php-job.zip'; + file_put_contents($jobFile, 'artifact'); + file_put_contents($jobFile . '.lock', ''); + + $payload = json_encode(['type' => 'php', 'job' => 'php-job.zip'], JSON_THROW_ON_ERROR); + $inputFile = $this->createInputFile($payload); + + http_response_code(200); + $controller = new DeletePendingJobController($inputFile, $this->tempDir); + ob_start(); + $controller->handle(); + $output = ob_get_clean(); + + static::assertSame(200, http_response_code()); + static::assertFalse(file_exists($jobFile)); + static::assertFalse(file_exists($jobFile . '.lock')); + static::assertJsonStringEqualsJsonString('{"status":"deleted"}', $output); + + unlink($inputFile); + } + + public function testDeletesWinlibsJobDirectory(): void + { + $winlibsDir = $this->tempDir . '/winlibs'; + mkdir($winlibsDir, 0755, true); + $jobDir = $winlibsDir . '/12345'; + mkdir($jobDir, 0755, true); + file_put_contents($jobDir . '/data.json', '{}'); + file_put_contents($jobDir . '.lock', ''); + + $payload = json_encode(['type' => 'winlibs', 'job' => '12345'], JSON_THROW_ON_ERROR); + $inputFile = $this->createInputFile($payload); + + http_response_code(200); + $controller = new DeletePendingJobController($inputFile, $this->tempDir); + ob_start(); + $controller->handle(); + ob_end_clean(); + + static::assertSame(200, http_response_code()); + static::assertFalse(is_dir($jobDir)); + static::assertFalse(file_exists($jobDir . '.lock')); + + unlink($inputFile); + } + + public function testReturns404WhenJobMissing(): void + { + $payload = json_encode(['type' => 'pecl', 'job' => 'missing.zip'], JSON_THROW_ON_ERROR); + $inputFile = $this->createInputFile($payload); + + http_response_code(200); + $controller = new DeletePendingJobController($inputFile, $this->tempDir); + ob_start(); + $controller->handle(); + $output = ob_get_clean(); + + static::assertSame(404, http_response_code()); + static::assertStringContainsString('Job not found', $output); + + unlink($inputFile); + } + + private function createInputFile(string $json): string + { + $tempFile = tempnam(sys_get_temp_dir(), 'delete-pending-input-'); + file_put_contents($tempFile, $json); + + return $tempFile; + } +} \ No newline at end of file

« previous php.webmaster (#33331) next »