Egg API Import/Delete (#1947)

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
Co-authored-by: Boy132 <mail@boy132.de>
This commit is contained in:
DaNussi 2025-12-16 12:28:12 +01:00 committed by GitHub
parent 4a1ecb1adc
commit 014e866d0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 122 additions and 44 deletions

View File

@ -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);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Http\Requests\Api\Application\Eggs;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Egg;
use App\Services\Acl\Api\AdminAcl;
class ImportEggRequest extends ApplicationApiRequest
{
protected ?string $resource = Egg::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE;
}

View File

@ -2,6 +2,7 @@
namespace App\Services\Eggs\Sharing;
use App\Enums\EggFormat;
use App\Exceptions\Service\InvalidFileUploadException;
use App\Models\Egg;
use App\Models\EggVariable;
@ -12,7 +13,6 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use JsonException;
use Ramsey\Uuid\Uuid;
use Spatie\TemporaryDirectory\TemporaryDirectory;
use stdClass;
use Symfony\Component\Yaml\Yaml;
use Throwable;
@ -32,6 +32,18 @@ class EggImporterService
public function __construct(protected ConnectionInterface $connection) {}
/**
* Take a JSON or YAML as string and parse it into a new egg.
*
* @throws InvalidFileUploadException|Throwable
*/
public function fromContent(string $content, EggFormat $format = EggFormat::YAML, ?Egg $egg = null): Egg
{
$parsed = $this->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<array-key, mixed> $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<array-key, mixed>
*
* @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) {

View File

@ -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');
});
/*