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