diff --git a/app/Filament/Resources/EggResource/Pages/EditEgg.php b/app/Filament/Resources/EggResource/Pages/EditEgg.php index f0ff69ce1..8f7fdca98 100644 --- a/app/Filament/Resources/EggResource/Pages/EditEgg.php +++ b/app/Filament/Resources/EggResource/Pages/EditEgg.php @@ -7,6 +7,7 @@ use App\Models\Egg; use Filament\Actions; use Filament\Resources\Pages\EditRecord; use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor; +use App\Services\Eggs\Sharing\EggExporterService; use Filament\Forms; use Filament\Forms\Form; @@ -205,8 +206,7 @@ class EditEgg extends EditRecord ->icon('tabler-download') ->label('Export Egg') ->color('primary') - // TODO uses old admin panel export service - ->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg['id']])), + ->url(fn (EggExporterService $service, Egg $egg) => $service->handle($egg->id)), $this->getSaveFormAction()->formId('form'), ]; } diff --git a/app/Http/Controllers/Admin/Eggs/EggController.php b/app/Http/Controllers/Admin/Eggs/EggController.php index 5485d1235..692ff7942 100644 --- a/app/Http/Controllers/Admin/Eggs/EggController.php +++ b/app/Http/Controllers/Admin/Eggs/EggController.php @@ -2,15 +2,15 @@ namespace App\Http\Controllers\Admin\Eggs; +use App\Exceptions\Service\Egg\NoParentConfigurationFoundException; use Illuminate\View\View; use App\Models\Egg; use Illuminate\Http\RedirectResponse; use Prologue\Alerts\AlertsMessageBag; use Illuminate\View\Factory as ViewFactory; use App\Http\Controllers\Controller; -use App\Services\Eggs\EggUpdateService; -use App\Services\Eggs\EggCreationService; use App\Http\Requests\Admin\Egg\EggFormRequest; +use Ramsey\Uuid\Uuid; class EggController extends Controller { @@ -19,8 +19,6 @@ class EggController extends Controller */ public function __construct( protected AlertsMessageBag $alert, - protected EggCreationService $creationService, - protected EggUpdateService $updateService, protected ViewFactory $view ) { } @@ -58,7 +56,16 @@ class EggController extends Controller $data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null); $data['author'] = $request->user()->email; - $egg = $this->creationService->handle($data); + $data['config_from'] = array_get($data, 'config_from'); + if (!is_null($data['config_from'])) { + $parentEgg = Egg::query()->find(array_get($data, 'config_from')); + throw_unless($parentEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id'))); + } + + $egg = Egg::query()->create(array_merge($data, [ + 'uuid' => Uuid::uuid4()->toString(), + ])); + $this->alert->success(trans('admin/eggs.notices.egg_created'))->flash(); return redirect()->route('admin.eggs.view', $egg->id); @@ -90,7 +97,13 @@ class EggController extends Controller $data = $request->validated(); $data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null); - $this->updateService->handle($egg, $data); + $eggId = array_get($data, 'config_from'); + $copiedFromEgg = Egg::query()->find($eggId); + + throw_unless($copiedFromEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id'))); + + $egg->update($data); + $this->alert->success(trans('admin/eggs.notices.updated'))->flash(); return redirect()->route('admin.eggs.view', $egg->id); diff --git a/app/Http/Controllers/Admin/Eggs/EggShareController.php b/app/Http/Controllers/Admin/Eggs/EggShareController.php index e938403db..993064b65 100644 --- a/app/Http/Controllers/Admin/Eggs/EggShareController.php +++ b/app/Http/Controllers/Admin/Eggs/EggShareController.php @@ -10,7 +10,6 @@ use Symfony\Component\HttpFoundation\Response; use App\Services\Eggs\Sharing\EggExporterService; use App\Services\Eggs\Sharing\EggImporterService; use App\Http\Requests\Admin\Egg\EggImportFormRequest; -use App\Services\Eggs\Sharing\EggUpdateImporterService; class EggShareController extends Controller { @@ -21,7 +20,6 @@ class EggShareController extends Controller protected AlertsMessageBag $alert, protected EggExporterService $exporterService, protected EggImporterService $importerService, - protected EggUpdateImporterService $updateImporterService ) { } @@ -61,7 +59,7 @@ class EggShareController extends Controller */ public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse { - $this->updateImporterService->fromFile($egg, $request->file('import_file')); + $this->importerService->fromFile($request->file('import_file'), $egg); $this->alert->success(trans('admin/eggs.notices.updated_via_import'))->flash(); return redirect()->route('admin.eggs.view', ['egg' => $egg]); diff --git a/app/Services/Eggs/EggCreationService.php b/app/Services/Eggs/EggCreationService.php deleted file mode 100644 index e4ab3dde5..000000000 --- a/app/Services/Eggs/EggCreationService.php +++ /dev/null @@ -1,29 +0,0 @@ -find(array_get($data, 'config_from')); - throw_unless($parentEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id'))); - } - - return Egg::query()->create(array_merge($data, [ - 'uuid' => Uuid::uuid4()->toString(), - ])); - } -} diff --git a/app/Services/Eggs/EggParserService.php b/app/Services/Eggs/EggParserService.php deleted file mode 100644 index 22025dc5c..000000000 --- a/app/Services/Eggs/EggParserService.php +++ /dev/null @@ -1,108 +0,0 @@ - 'server.allocations.default.ip', - 'server.build.default.ip' => 'server.allocations.default.ip', - 'server.build.env.SERVER_PORT' => 'server.allocations.default.port', - 'server.build.default.port' => 'server.allocations.default.port', - 'server.build.env.SERVER_MEMORY' => 'server.build.memory_limit', - 'server.build.memory' => 'server.build.memory_limit', - 'server.build.env.' => 'server.environment.', - 'server.build.environment.' => 'server.environment.', - ]; - - /** - * Takes an uploaded file and parses out the egg configuration from within. - * - * @throws \JsonException - * @throws \App\Exceptions\Service\InvalidFileUploadException - */ - public function handle(UploadedFile $file): array - { - if ($file->getError() !== UPLOAD_ERR_OK) { - throw new InvalidFileUploadException('The selected file was not uploaded successfully'); - } - - $parsed = json_decode($file->getContent(), true, 512, JSON_THROW_ON_ERROR); - - $version = $parsed['meta']['version'] ?? ''; - - $parsed = match ($version) { - 'PTDL_v1' => $this->convertToV2($parsed), - 'PTDL_v2' => $parsed, - default => throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.') - }; - - // Make sure we only use recent variable format from now on - $parsed['config']['files'] = str_replace( - array_keys(self::UPGRADE_VARIABLES), - array_values(self::UPGRADE_VARIABLES), - $parsed['config']['files'] ?? '', - ); - - return $parsed; - } - - /** - * Fills the provided model with the parsed JSON data. - */ - public function fillFromParsed(Egg $model, array $parsed): Egg - { - return $model->forceFill([ - 'name' => Arr::get($parsed, 'name'), - 'description' => Arr::get($parsed, 'description'), - 'features' => Arr::get($parsed, 'features'), - 'docker_images' => Arr::get($parsed, 'docker_images'), - 'file_denylist' => Collection::make(Arr::get($parsed, 'file_denylist')) - ->filter(fn ($value) => !empty($value)), - 'update_url' => Arr::get($parsed, 'meta.update_url'), - 'config_files' => Arr::get($parsed, 'config.files'), - 'config_startup' => Arr::get($parsed, 'config.startup'), - 'config_logs' => Arr::get($parsed, 'config.logs'), - 'config_stop' => Arr::get($parsed, 'config.stop'), - 'startup' => Arr::get($parsed, 'startup'), - 'script_install' => Arr::get($parsed, 'scripts.installation.script'), - 'script_entry' => Arr::get($parsed, 'scripts.installation.entrypoint'), - 'script_container' => Arr::get($parsed, 'scripts.installation.container'), - ]); - } - - /** - * Converts a PTDL_V1 egg into the expected PTDL_V2 egg format. This just handles - * the "docker_images" field potentially not being present, and not being in the - * expected "key => value" format. - */ - protected function convertToV2(array $parsed): array - { - // Maintain backwards compatability for eggs that are still using the old single image - // string format. New eggs can provide an array of Docker images that can be used. - if (!isset($parsed['images'])) { - $images = [Arr::get($parsed, 'image') ?? 'nil']; - } else { - $images = $parsed['images']; - } - - unset($parsed['images'], $parsed['image']); - - $parsed['docker_images'] = []; - foreach ($images as $image) { - $parsed['docker_images'][$image] = $image; - } - - $parsed['variables'] = array_map(function ($value) { - return array_merge($value, ['field_type' => 'text']); - }, $parsed['variables']); - - return $parsed; - } -} diff --git a/app/Services/Eggs/EggUpdateService.php b/app/Services/Eggs/EggUpdateService.php deleted file mode 100644 index 5591b3181..000000000 --- a/app/Services/Eggs/EggUpdateService.php +++ /dev/null @@ -1,26 +0,0 @@ -find($eggId); - - throw_unless($copiedFromEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id'))); - - // TODO: Once the admin UI is done being reworked and this is exposed - // in said UI, remove this so that you can actually update the denylist. - unset($data['file_denylist']); - - $egg->update($data); - } -} diff --git a/app/Services/Eggs/Sharing/EggImporterService.php b/app/Services/Eggs/Sharing/EggImporterService.php index 5c94ec2fa..1e85923d7 100644 --- a/app/Services/Eggs/Sharing/EggImporterService.php +++ b/app/Services/Eggs/Sharing/EggImporterService.php @@ -2,18 +2,30 @@ namespace App\Services\Eggs\Sharing; +use App\Exceptions\Service\InvalidFileUploadException; use Ramsey\Uuid\Uuid; use Illuminate\Support\Arr; use App\Models\Egg; use Illuminate\Http\UploadedFile; use App\Models\EggVariable; use Illuminate\Database\ConnectionInterface; -use App\Services\Eggs\EggParserService; +use Illuminate\Support\Collection; use Spatie\TemporaryDirectory\TemporaryDirectory; class EggImporterService { - public function __construct(protected ConnectionInterface $connection, protected EggParserService $parser) + public const UPGRADE_VARIABLES = [ + 'server.build.env.SERVER_IP' => 'server.allocations.default.ip', + 'server.build.default.ip' => 'server.allocations.default.ip', + 'server.build.env.SERVER_PORT' => 'server.allocations.default.port', + 'server.build.default.port' => 'server.allocations.default.port', + 'server.build.env.SERVER_MEMORY' => 'server.build.memory_limit', + 'server.build.memory' => 'server.build.memory_limit', + 'server.build.env.' => 'server.environment.', + 'server.build.environment.' => 'server.environment.', + ]; + + public function __construct(protected ConnectionInterface $connection) { } @@ -22,13 +34,13 @@ class EggImporterService * * @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable */ - public function fromFile(UploadedFile $file): Egg + public function fromFile(UploadedFile $file, Egg $egg = null): Egg { - $parsed = $this->parser->handle($file); + $parsed = $this->parseFile($file); - return $this->connection->transaction(function () use ($parsed) { + return $this->connection->transaction(function () use ($egg, $parsed) { $uuid = $parsed['uuid'] ?? Uuid::uuid4()->toString(); - $egg = Egg::where('uuid', $uuid)->first() ?? new Egg(); + $egg = $egg ?? Egg::where('uuid', $uuid)->first() ?? new Egg(); $egg = $egg->forceFill([ 'uuid' => $uuid, @@ -36,23 +48,32 @@ class EggImporterService 'copy_script_from' => null, ]); - $egg = $this->parser->fillFromParsed($egg, $parsed); + $egg = $this->fillFromParsed($egg, $parsed); $egg->save(); + // Update existing variables or create new ones. foreach ($parsed['variables'] ?? [] as $variable) { - EggVariable::query()->forceCreate(array_merge($variable, ['egg_id' => $egg->id])); + EggVariable::unguarded(function () use ($egg, $variable) { + $egg->variables()->updateOrCreate([ + 'env_variable' => $variable['env_variable'], + ], Collection::make($variable)->except(['egg_id', 'env_variable'])->toArray()); + }); } - return $egg; + $imported = array_map(fn ($value) => $value['env_variable'], $parsed['variables'] ?? []); + + $egg->variables()->whereNotIn('env_variable', $imported)->delete(); + + return $egg->refresh(); }); } /** - * Take an url and parse it into a new egg. + * Take an url and parse it into a new egg or update an existing one. * * @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable */ - public function fromUrl(string $url): Egg + public function fromUrl(string $url, Egg $egg = null): Egg { $info = pathinfo($url); $tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed(); @@ -60,6 +81,91 @@ class EggImporterService file_put_contents($tmpPath, file_get_contents($url)); - return $this->fromFile(new UploadedFile($tmpPath, $info['basename'], 'application/json')); + return $this->fromFile(new UploadedFile($tmpPath, $info['basename'], 'application/json'), $egg); + } + + /** + * Takes an uploaded file and parses out the egg configuration from within. + * + * @throws \JsonException + * @throws \App\Exceptions\Service\InvalidFileUploadException + */ + protected function parseFile(UploadedFile $file): array + { + if ($file->getError() !== UPLOAD_ERR_OK) { + throw new InvalidFileUploadException('The selected file was not uploaded successfully'); + } + + $parsed = json_decode($file->getContent(), true, 512, JSON_THROW_ON_ERROR); + + $version = $parsed['meta']['version'] ?? ''; + + $parsed = match ($version) { + 'PTDL_v1' => $this->convertToV2($parsed), + 'PTDL_v2' => $parsed, + default => throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.') + }; + + // Make sure we only use recent variable format from now on + $parsed['config']['files'] = str_replace( + array_keys(self::UPGRADE_VARIABLES), + array_values(self::UPGRADE_VARIABLES), + $parsed['config']['files'] ?? '', + ); + + return $parsed; + } + + /** + * Fills the provided model with the parsed JSON data. + */ + protected function fillFromParsed(Egg $model, array $parsed): Egg + { + return $model->forceFill([ + 'name' => Arr::get($parsed, 'name'), + 'description' => Arr::get($parsed, 'description'), + 'features' => Arr::get($parsed, 'features'), + 'docker_images' => Arr::get($parsed, 'docker_images'), + 'file_denylist' => Collection::make(Arr::get($parsed, 'file_denylist')) + ->filter(fn ($value) => !empty($value)), + 'update_url' => Arr::get($parsed, 'meta.update_url'), + 'config_files' => Arr::get($parsed, 'config.files'), + 'config_startup' => Arr::get($parsed, 'config.startup'), + 'config_logs' => Arr::get($parsed, 'config.logs'), + 'config_stop' => Arr::get($parsed, 'config.stop'), + 'startup' => Arr::get($parsed, 'startup'), + 'script_install' => Arr::get($parsed, 'scripts.installation.script'), + 'script_entry' => Arr::get($parsed, 'scripts.installation.entrypoint'), + 'script_container' => Arr::get($parsed, 'scripts.installation.container'), + ]); + } + + /** + * Converts a PTDL_V1 egg into the expected PTDL_V2 egg format. This just handles + * the "docker_images" field potentially not being present, and not being in the + * expected "key => value" format. + */ + protected function convertToV2(array $parsed): array + { + // Maintain backwards compatability for eggs that are still using the old single image + // string format. New eggs can provide an array of Docker images that can be used. + if (!isset($parsed['images'])) { + $images = [Arr::get($parsed, 'image') ?? 'nil']; + } else { + $images = $parsed['images']; + } + + unset($parsed['images'], $parsed['image']); + + $parsed['docker_images'] = []; + foreach ($images as $image) { + $parsed['docker_images'][$image] = $image; + } + + $parsed['variables'] = array_map(function ($value) { + return array_merge($value, ['field_type' => 'text']); + }, $parsed['variables']); + + return $parsed; } } diff --git a/app/Services/Eggs/Sharing/EggUpdateImporterService.php b/app/Services/Eggs/Sharing/EggUpdateImporterService.php deleted file mode 100644 index a441079e5..000000000 --- a/app/Services/Eggs/Sharing/EggUpdateImporterService.php +++ /dev/null @@ -1,67 +0,0 @@ -parser->handle($file); - - return $this->connection->transaction(function () use ($egg, $parsed) { - $egg = $this->parser->fillFromParsed($egg, $parsed); - $egg->save(); - - // Update existing variables or create new ones. - foreach ($parsed['variables'] ?? [] as $variable) { - EggVariable::unguarded(function () use ($egg, $variable) { - $egg->variables()->updateOrCreate([ - 'env_variable' => $variable['env_variable'], - ], Collection::make($variable)->except(['egg_id', 'env_variable'])->toArray()); - }); - } - - $imported = array_map(fn ($value) => $value['env_variable'], $parsed['variables'] ?? []); - - $egg->variables()->whereNotIn('env_variable', $imported)->delete(); - - return $egg->refresh(); - }); - } - - /** - * Update an existing Egg using an url. - * - * @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable - */ - public function fromUrl(Egg $egg, string $url): Egg - { - $info = pathinfo($url); - $tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed(); - $tmpPath = $tmpDir->path($info['basename']); - - file_put_contents($tmpPath, file_get_contents($url)); - - return $this->fromFile($egg, new UploadedFile($tmpPath, $info['basename'], 'application/json')); - } -} diff --git a/composer.json b/composer.json index 833cef865..481fa145d 100644 --- a/composer.json +++ b/composer.json @@ -88,4 +88,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} +} \ No newline at end of file diff --git a/database/Seeders/EggSeeder.php b/database/Seeders/EggSeeder.php index afcb55a6d..dcbc4f25c 100644 --- a/database/Seeders/EggSeeder.php +++ b/database/Seeders/EggSeeder.php @@ -7,14 +7,11 @@ use Exception; use Illuminate\Database\Seeder; use Illuminate\Http\UploadedFile; use App\Services\Eggs\Sharing\EggImporterService; -use App\Services\Eggs\Sharing\EggUpdateImporterService; class EggSeeder extends Seeder { protected EggImporterService $importerService; - protected EggUpdateImporterService $updateImporterService; - /** * @var string[] */ @@ -29,11 +26,9 @@ class EggSeeder extends Seeder * EggSeeder constructor. */ public function __construct( - EggImporterService $importerService, - EggUpdateImporterService $updateImporterService + EggImporterService $importerService ) { $this->importerService = $importerService; - $this->updateImporterService = $updateImporterService; } /** @@ -75,7 +70,7 @@ class EggSeeder extends Seeder ->first(); if ($egg instanceof Egg) { - $this->updateImporterService->fromFile($egg, $file); + $this->importerService->fromFile($file, $egg); $this->command->info('Updated ' . $decoded['name']); } else { $this->importerService->fromFile($file);