From 014e866d0ef2ab1482671d114cf6979c5b2de810 Mon Sep 17 00:00:00 2001 From: DaNussi <107034782+DaNussi@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:28:12 +0100 Subject: [PATCH] Egg API Import/Delete (#1947) Co-authored-by: Boy132 Co-authored-by: Boy132 --- .../Api/Application/Eggs/EggController.php | 39 +++++++ .../Api/Application/Eggs/ImportEggRequest.php | 14 +++ .../Eggs/Sharing/EggImporterService.php | 110 +++++++++++------- routes/api-application.php | 3 + 4 files changed, 122 insertions(+), 44 deletions(-) create mode 100644 app/Http/Requests/Api/Application/Eggs/ImportEggRequest.php diff --git a/app/Http/Controllers/Api/Application/Eggs/EggController.php b/app/Http/Controllers/Api/Application/Eggs/EggController.php index 8ff9dfc7b..c3ce0c026 100644 --- a/app/Http/Controllers/Api/Application/Eggs/EggController.php +++ b/app/Http/Controllers/Api/Application/Eggs/EggController.php @@ -7,15 +7,22 @@ use App\Http\Controllers\Api\Application\ApplicationApiController; use App\Http\Requests\Api\Application\Eggs\ExportEggRequest; use App\Http\Requests\Api\Application\Eggs\GetEggRequest; use App\Http\Requests\Api\Application\Eggs\GetEggsRequest; +use App\Http\Requests\Api\Application\Eggs\ImportEggRequest; use App\Models\Egg; use App\Services\Eggs\Sharing\EggExporterService; +use App\Services\Eggs\Sharing\EggImporterService; use App\Transformers\Api\Application\EggTransformer; +use Exception; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Response; use Symfony\Component\HttpFoundation\StreamedResponse; +use Throwable; class EggController extends ApplicationApiController { public function __construct( private EggExporterService $exporterService, + private EggImporterService $importService ) { parent::__construct(); } @@ -48,6 +55,20 @@ class EggController extends ApplicationApiController ->toArray(); } + /** + * Delete egg + * + * Delete an egg from the Panel. + * + * @throws Exception + */ + public function delete(GetEggRequest $request, Egg $egg): Response + { + $egg->delete(); + + return $this->returnNoContent(); + } + /** * Export egg * @@ -63,4 +84,22 @@ class EggController extends ApplicationApiController 'Content-Type' => 'application/' . $format->value, ]); } + + /** + * Import egg + * + * Create a new egg on the Panel. Returns the created egg and an HTTP/201 status response on success + * If no uuid is supplied a new one will be generated + * If an uuid is supplied, and it already exists the old configuration get overwritten + * + * @throws Exception|Throwable + */ + public function import(ImportEggRequest $request): JsonResponse + { + $egg = $this->importService->fromContent($request->getContent()); + + return $this->fractal->item($egg) + ->transformWith($this->getTransformer(EggTransformer::class)) + ->respond(201); + } } diff --git a/app/Http/Requests/Api/Application/Eggs/ImportEggRequest.php b/app/Http/Requests/Api/Application/Eggs/ImportEggRequest.php new file mode 100644 index 000000000..a1980f04f --- /dev/null +++ b/app/Http/Requests/Api/Application/Eggs/ImportEggRequest.php @@ -0,0 +1,14 @@ +parse($content, $format); + + return $this->fromParsed($parsed, $egg); + } + /** * Take an uploaded JSON or YAML file and parse it into a new egg. * @@ -39,8 +51,56 @@ class EggImporterService */ public function fromFile(UploadedFile $file, ?Egg $egg = null): Egg { - $parsed = $this->parseFile($file); + if ($file->getError() !== UPLOAD_ERR_OK) { + throw new InvalidFileUploadException('The selected file was not uploaded successfully'); + } + $extension = strtolower($file->getClientOriginalExtension()); + $mime = $file->getMimeType(); + + try { + $content = $file->getContent(); + + if (in_array($extension, ['yaml', 'yml']) || str_contains($mime, 'yaml')) { + return $this->fromContent($content, EggFormat::YAML, $egg); + } + + return $this->fromContent($content, EggFormat::JSON, $egg); + } catch (Throwable $e) { + throw new InvalidFileUploadException('File parse failed: ' . $e->getMessage()); + } + } + + /** + * Take a URL (YAML or JSON) and parse it into a new egg or update an existing one. + * + * @throws InvalidFileUploadException|Throwable + */ + public function fromUrl(string $url, ?Egg $egg = null): Egg + { + $info = pathinfo($url); + $extension = strtolower($info['extension']); + + $format = match ($extension) { + 'yaml', 'yml' => EggFormat::YAML, + 'json' => EggFormat::JSON, + default => throw new InvalidFileUploadException('Unsupported file format.'), + }; + + $content = Http::timeout(5)->connectTimeout(1)->get($url)->throw()->body(); + + return $this->fromContent($content, $format, $egg); + } + + /** + * Take an array and parse it into a new egg. + * + * @param array $parsed + * + * @throws InvalidFileUploadException|Throwable + */ + protected function fromParsed(array $parsed, ?Egg $egg = null): Egg + { return $this->connection->transaction(function () use ($egg, $parsed) { $uuid = $parsed['uuid'] ?? Uuid::uuid4()->toString(); $egg = $egg ?? Egg::where('uuid', $uuid)->first() ?? new Egg(); @@ -76,55 +136,17 @@ class EggImporterService } /** - * Take a URL (YAML or JSON) and parse it into a new egg or update an existing one. - * - * @throws InvalidFileUploadException|Throwable - */ - public function fromUrl(string $url, ?Egg $egg = null): Egg - { - $info = pathinfo($url); - $extension = strtolower($info['extension']); - - $tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed(); - $tmpPath = $tmpDir->path($info['basename']); - - $fileContents = Http::timeout(5)->connectTimeout(1)->get($url)->throw()->body(); - - if (!$fileContents || !file_put_contents($tmpPath, $fileContents)) { - throw new InvalidFileUploadException('Could not download or write temporary file.'); - } - - $mime = match ($extension) { - 'yaml', 'yml' => 'application/yaml', - 'json' => 'application/json', - default => throw new InvalidFileUploadException('Unsupported file format.'), - }; - - return $this->fromFile(new UploadedFile($tmpPath, $info['basename'], $mime), $egg); - } - - /** - * Takes an uploaded file and parses out the egg configuration from within. + * Takes a string and parses out the egg configuration from within. * * @return array * * @throws InvalidFileUploadException|JsonException */ - protected function parseFile(UploadedFile $file): array + protected function parse(string $content, EggFormat $format): array { - if ($file->getError() !== UPLOAD_ERR_OK) { - throw new InvalidFileUploadException('The selected file was not uploaded successfully'); - } - - $extension = strtolower($file->getClientOriginalExtension()); - $mime = $file->getMimeType(); - try { - $content = $file->getContent(); - - $parsed = match (true) { - in_array($extension, ['yaml', 'yml']), - str_contains($mime, 'yaml') => Yaml::parse($content), + $parsed = match ($format) { + EggFormat::YAML => Yaml::parse($content), default => json_decode($content, true, 512, JSON_THROW_ON_ERROR), }; } catch (Throwable $e) { diff --git a/routes/api-application.php b/routes/api-application.php index 953aca4d7..07e20b51d 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -102,6 +102,9 @@ Route::prefix('/eggs')->group(function () { Route::get('/', [Application\Eggs\EggController::class, 'index'])->name('api.application.eggs.eggs'); Route::get('/{egg:id}', [Application\Eggs\EggController::class, 'view'])->name('api.application.eggs.eggs.view'); Route::get('/{egg:id}/export', [Application\Eggs\EggController::class, 'export'])->name('api.application.eggs.eggs.export'); + Route::post('/import', [Application\Eggs\EggController::class, 'import'])->name('api.application.eggs.eggs.import'); + Route::delete('/{egg:id}', [Application\Eggs\EggController::class, 'delete'])->name('api.application.eggs.eggs.delete'); + Route::delete('/uuid/{egg:uuid}', [Application\Eggs\EggController::class, 'delete'])->name('api.application.eggs.eggs.delete.uuid'); }); /*