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

[email protected] (Shivam Mathur)
Newsgroups php.webmaster
Message-ID <[email protected]>
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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.