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

Reply via email to