diff --git a/app/Exceptions/Service/Egg/BadYamlFormatException.php b/app/Exceptions/Service/Egg/BadYamlFormatException.php new file mode 100644 index 000000000..cb5949de0 --- /dev/null +++ b/app/Exceptions/Service/Egg/BadYamlFormatException.php @@ -0,0 +1,9 @@ +repository = $repository; - $this->nestCreationService = $nestCreationService; $this->nestDeletionService = $nestDeletionService; $this->nestUpdateService = $nestUpdateService; + $this->eggImporterService = $eggImporterService; } /** @@ -94,10 +95,25 @@ class NestController extends ApplicationApiController ->toArray(); } + /** + * Imports an egg. + */ + public function import(ImportEggRequest $request, Nest $nest): array + { + $egg = $this->eggImporterService->handleContent( + $nest->id, + $request->getContent(), + $request->headers->get('Content-Type'), + ); + + return $this->fractal->item($egg) + ->transformWith(EggTransformer::class) + ->toArray(); + } + /** * Updates an existing nest. * - * @throws \Illuminate\Contracts\Container\BindingResolutionException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ 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..3751e0976 --- /dev/null +++ b/app/Http/Requests/Api/Application/Eggs/ImportEggRequest.php @@ -0,0 +1,9 @@ +handleFile($nestId, $file); + } + + /** + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException + * @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException + * @throws \Pterodactyl\Exceptions\Service\Egg\BadYamlFormatException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function handleFile(int $nestId, UploadedFile $file): Egg { if ($file->getError() !== UPLOAD_ERR_OK || !$file->isFile()) { throw new InvalidFileUploadException(sprintf('The selected file ["%s"] was not in a valid format to import. (is_file: %s is_valid: %s err_code: %s err: %s)', $file->getFilename(), $file->isFile() ? 'true' : 'false', $file->isValid() ? 'true' : 'false', $file->getError(), $file->getErrorMessage())); } - /** @var array $parsed */ - $parsed = json_decode($file->openFile()->fread($file->getSize()), true); - if (json_last_error() !== 0) { - throw new BadJsonFormatException(trans('exceptions.nest.importer.json_error', ['error' => json_last_error_msg()])); - } + return $this->handleContent($nestId, $file->openFile()->fread($file->getSize()), 'application/json'); + } + /** + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException + * @throws \Pterodactyl\Exceptions\Service\Egg\BadYamlFormatException + * @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handleContent(int $nestId, string $content, string $contentType): Egg + { + switch (true) { + case strpos($contentType, 'application/json') === 0: + $parsed = json_decode($content, true); + if (json_last_error() !== 0) { + throw new BadJsonFormatException(trans('exceptions.nest.importer.json_error', ['error' => json_last_error_msg()])); + } + + return $this->handleArray($nestId, $parsed); + case strpos($contentType, 'application/yaml') === 0: + try { + $parsed = Yaml::parse($content); + + return $this->handleArray($nestId, $parsed); + } catch (ParseException $exception) { + throw new BadYamlFormatException('There was an error while attempting to parse the YAML: ' . $exception->getMessage() . '.'); + } + default: + throw new DisplayException('unknown content type'); + } + } + + /** + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException + */ + private function handleArray(int $nestId, array $parsed): Egg + { if (Arr::get($parsed, 'meta.version') !== 'PTDL_v1') { throw new InvalidFileUploadException(trans('exceptions.nest.importer.invalid_json_provided')); } - $nest = $this->nestRepository->getWithEggs($nest); + $nest = $this->nestRepository->getWithEggs($nestId); $this->connection->beginTransaction(); /** @var \Pterodactyl\Models\Egg $egg */ diff --git a/database/Seeders/EggSeeder.php b/database/Seeders/EggSeeder.php index 4c7697cf0..2e3ed594a 100644 --- a/database/Seeders/EggSeeder.php +++ b/database/Seeders/EggSeeder.php @@ -123,7 +123,7 @@ class EggSeeder extends Seeder $this->command->info('Updated ' . $decoded->name); } catch (RecordNotFoundException $exception) { - $this->importerService->handle($file, $nest->id); + $this->importerService->handleFile($nest->id, $file); $this->command->comment('Created ' . $decoded->name); } diff --git a/resources/scripts/api/admin/nests/importEgg.ts b/resources/scripts/api/admin/nests/importEgg.ts new file mode 100644 index 000000000..2163386ca --- /dev/null +++ b/resources/scripts/api/admin/nests/importEgg.ts @@ -0,0 +1,17 @@ +import http from '@/api/http'; +import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg'; + +export default (id: number, content: any, type = 'application/json', include: string[] = []): Promise => { + return new Promise((resolve, reject) => { + http.post(`/api/application/nests/${id}/import`, content, { + headers: { + 'Content-Type': type, + }, + params: { + include: include.join(','), + }, + }) + .then(({ data }) => resolve(rawDataToEgg(data))) + .catch(reject); + }); +}; diff --git a/resources/scripts/components/admin/nests/ImportEggButton.tsx b/resources/scripts/components/admin/nests/ImportEggButton.tsx index 292db9248..4cd0bab60 100644 --- a/resources/scripts/components/admin/nests/ImportEggButton.tsx +++ b/resources/scripts/components/admin/nests/ImportEggButton.tsx @@ -1,14 +1,37 @@ +import getEggs from '@/api/admin/nests/getEggs'; +import importEgg from '@/api/admin/nests/importEgg'; +import useFlash from '@/plugins/useFlash'; import { jsonLanguage } from '@codemirror/lang-json'; import Editor from '@/components/elements/Editor'; import React, { useState } from 'react'; import Button from '@/components/elements/Button'; import Modal from '@/components/elements/Modal'; import FlashMessageRender from '@/components/FlashMessageRender'; +import { useRouteMatch } from 'react-router-dom'; import tw from 'twin.macro'; export default ({ className }: { className?: string }) => { const [ visible, setVisible ] = useState(false); + const { clearFlashes } = useFlash(); + + const match = useRouteMatch<{ nestId: string }>(); + const { mutate } = getEggs(Number(match.params.nestId)); + + let fetchFileContent: null | (() => Promise) = null; + + const submit = async () => { + clearFlashes('egg:import'); + + if (fetchFileContent === null) { + return; + } + + const egg = await importEgg(Number(match.params.nestId), await fetchFileContent()); + await mutate(data => ({ ...data!, items: [ ...data!.items!, egg ] })); + setVisible(false); + }; + return ( <> {

Import Egg

- + { + fetchFileContent = value; + }} + />
-
diff --git a/routes/api-application.php b/routes/api-application.php index 85fcf11d0..d9000f59f 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -98,6 +98,7 @@ Route::group(['prefix' => '/nests'], function () { Route::get('/{nest}/eggs', [\Pterodactyl\Http\Controllers\Api\Application\Eggs\EggController::class, 'index']); Route::post('/', [\Pterodactyl\Http\Controllers\Api\Application\Nests\NestController::class, 'store']); + Route::post('/{nest}/import', [\Pterodactyl\Http\Controllers\Api\Application\Nests\NestController::class, 'import']); Route::patch('/{nest}', [\Pterodactyl\Http\Controllers\Api\Application\Nests\NestController::class, 'update']);