Finalize service option import/export
This commit is contained in:
parent
d608c313c3
commit
6269a08db7
16 changed files with 405 additions and 271 deletions
|
@ -33,6 +33,16 @@ interface ServiceOptionRepositoryInterface extends RepositoryInterface
|
||||||
*/
|
*/
|
||||||
public function getWithCopyAttributes(int $id): ServiceOption;
|
public function getWithCopyAttributes(int $id): ServiceOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all of the data needed to export a service.
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @return \Pterodactyl\Models\ServiceOption
|
||||||
|
*
|
||||||
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
|
*/
|
||||||
|
public function getWithExportAttributes(int $id): ServiceOption;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirm a copy script belongs to the same service as the item trying to use it.
|
* Confirm a copy script belongs to the same service as the item trying to use it.
|
||||||
*
|
*
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
namespace Pterodactyl\Contracts\Repository;
|
namespace Pterodactyl\Contracts\Repository;
|
||||||
|
|
||||||
use Pterodactyl\Models\Service;
|
use Pterodactyl\Models\Service;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
interface ServiceRepositoryInterface extends RepositoryInterface
|
interface ServiceRepositoryInterface extends RepositoryInterface
|
||||||
{
|
{
|
||||||
|
@ -18,23 +17,29 @@ interface ServiceRepositoryInterface extends RepositoryInterface
|
||||||
* Return a service or all services with their associated options, variables, and packs.
|
* Return a service or all services with their associated options, variables, and packs.
|
||||||
*
|
*
|
||||||
* @param int $id
|
* @param int $id
|
||||||
* @return \Illuminate\Support\Collection
|
* @return \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Service
|
||||||
|
*
|
||||||
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
*/
|
*/
|
||||||
public function getWithOptions(int $id = null): Collection;
|
public function getWithOptions(int $id = null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a service or all services and the count of options, packs, and servers for that service.
|
* Return a service or all services and the count of options, packs, and servers for that service.
|
||||||
*
|
*
|
||||||
* @param int|null $id
|
* @param int|null $id
|
||||||
* @return \Illuminate\Support\Collection
|
* @return \Pterodactyl\Models\Service|\Illuminate\Database\Eloquent\Collection
|
||||||
|
*
|
||||||
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
*/
|
*/
|
||||||
public function getWithCounts(int $id = null): Collection;
|
public function getWithCounts(int $id = null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a service along with its associated options and the servers relation on those options.
|
* Return a service along with its associated options and the servers relation on those options.
|
||||||
*
|
*
|
||||||
* @param int $id
|
* @param int $id
|
||||||
* @return mixed
|
* @return \Pterodactyl\Models\Service
|
||||||
|
*
|
||||||
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
*/
|
*/
|
||||||
public function getWithOptionServers(int $id): Service;
|
public function getWithOptionServers(int $id): Service;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Pterodactyl - Panel
|
||||||
|
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
|
||||||
|
*
|
||||||
|
* This software is licensed under the terms of the MIT license.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Pterodactyl\Exceptions\Service\ServiceOption;
|
||||||
|
|
||||||
|
use Pterodactyl\Exceptions\DisplayException;
|
||||||
|
|
||||||
|
class DuplicateOptionTagException extends DisplayException
|
||||||
|
{
|
||||||
|
}
|
|
@ -9,26 +9,38 @@
|
||||||
|
|
||||||
namespace Pterodactyl\Http\Controllers\Admin\Services\Options;
|
namespace Pterodactyl\Http\Controllers\Admin\Services\Options;
|
||||||
|
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Pterodactyl\Models\ServiceOption;
|
use Pterodactyl\Models\ServiceOption;
|
||||||
use Pterodactyl\Http\Controllers\Controller;
|
use Pterodactyl\Http\Controllers\Controller;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Pterodactyl\Services\Services\Exporter\XMLExporterService;
|
use Pterodactyl\Http\Requests\Admin\Service\OptionImportFormRequest;
|
||||||
|
use Pterodactyl\Services\Services\Sharing\ServiceOptionExporterService;
|
||||||
|
use Pterodactyl\Services\Services\Sharing\ServiceOptionImporterService;
|
||||||
|
|
||||||
class OptionShareController extends Controller
|
class OptionShareController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var \Pterodactyl\Services\Services\Exporter\XMLExporterService
|
* @var \Pterodactyl\Services\Services\Sharing\ServiceOptionExporterService
|
||||||
*/
|
*/
|
||||||
protected $exporterService;
|
protected $exporterService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Pterodactyl\Services\Services\Sharing\ServiceOptionImporterService
|
||||||
|
*/
|
||||||
|
protected $importerService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OptionShareController constructor.
|
* OptionShareController constructor.
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Services\Services\Exporter\XMLExporterService $exporterService
|
* @param \Pterodactyl\Services\Services\Sharing\ServiceOptionExporterService $exporterService
|
||||||
|
* @param \Pterodactyl\Services\Services\Sharing\ServiceOptionImporterService $importerService
|
||||||
*/
|
*/
|
||||||
public function __construct(XMLExporterService $exporterService)
|
public function __construct(
|
||||||
{
|
ServiceOptionExporterService $exporterService,
|
||||||
|
ServiceOptionImporterService $importerService
|
||||||
|
) {
|
||||||
$this->exporterService = $exporterService;
|
$this->exporterService = $exporterService;
|
||||||
|
$this->importerService = $importerService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,8 +54,25 @@ class OptionShareController extends Controller
|
||||||
return response($this->exporterService->handle($option->id), 200, [
|
return response($this->exporterService->handle($option->id), 200, [
|
||||||
'Content-Transfer-Encoding' => 'binary',
|
'Content-Transfer-Encoding' => 'binary',
|
||||||
'Content-Description' => 'File Transfer',
|
'Content-Description' => 'File Transfer',
|
||||||
'Content-Disposition' => 'attachment; filename=' . kebab_case($option->name) . '.xml',
|
'Content-Disposition' => 'attachment; filename=' . kebab_case($option->name) . '.json',
|
||||||
'Content-Type' => 'application/xml',
|
'Content-Type' => 'application/json',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a new service option using an XML file.
|
||||||
|
*
|
||||||
|
* @param \Pterodactyl\Http\Requests\Admin\Service\OptionImportFormRequest $request
|
||||||
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
|
*
|
||||||
|
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||||
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
|
* @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException
|
||||||
|
*/
|
||||||
|
public function import(OptionImportFormRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$option = $this->importerService->handle($request->file('import_file'), $request->input('import_to_service'));
|
||||||
|
|
||||||
|
return redirect()->route('admin.services.option.view', ['option' => $option->id]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
26
app/Http/Requests/Admin/Service/OptionImportFormRequest.php
Normal file
26
app/Http/Requests/Admin/Service/OptionImportFormRequest.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Pterodactyl - Panel
|
||||||
|
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
|
||||||
|
*
|
||||||
|
* This software is licensed under the terms of the MIT license.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Pterodactyl\Http\Requests\Admin\Service;
|
||||||
|
|
||||||
|
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
|
||||||
|
|
||||||
|
class OptionImportFormRequest extends AdminFormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'import_file' => 'bail|required|file|max:1000|mimetypes:application/json,text/plain',
|
||||||
|
'import_to_service' => 'bail|required|integer|exists:services,id',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,7 +65,6 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract
|
||||||
*/
|
*/
|
||||||
protected static $applicationRules = [
|
protected static $applicationRules = [
|
||||||
'service_id' => 'required',
|
'service_id' => 'required',
|
||||||
'author' => 'required',
|
|
||||||
'name' => 'required',
|
'name' => 'required',
|
||||||
'description' => 'required',
|
'description' => 'required',
|
||||||
'tag' => 'required',
|
'tag' => 'required',
|
||||||
|
@ -83,10 +82,10 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract
|
||||||
*/
|
*/
|
||||||
protected static $dataIntegrityRules = [
|
protected static $dataIntegrityRules = [
|
||||||
'service_id' => 'bail|numeric|exists:services,id',
|
'service_id' => 'bail|numeric|exists:services,id',
|
||||||
'author' => 'email',
|
'uuid' => 'string|size:36',
|
||||||
'name' => 'string|max:255',
|
'name' => 'string|max:255',
|
||||||
'description' => 'string',
|
'description' => 'string',
|
||||||
'tag' => 'bail|alpha_num|max:60|unique:service_options,tag',
|
'tag' => 'bail|string|max:150',
|
||||||
'docker_image' => 'string|max:255',
|
'docker_image' => 'string|max:255',
|
||||||
'startup' => 'nullable|string',
|
'startup' => 'nullable|string',
|
||||||
'config_from' => 'bail|nullable|numeric|exists:service_options,id',
|
'config_from' => 'bail|nullable|numeric|exists:service_options,id',
|
||||||
|
|
|
@ -61,6 +61,25 @@ class ServiceOptionRepository extends EloquentRepository implements ServiceOptio
|
||||||
return $instance;
|
return $instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all of the data needed to export a service.
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @return \Pterodactyl\Models\ServiceOption
|
||||||
|
*
|
||||||
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
|
*/
|
||||||
|
public function getWithExportAttributes(int $id): ServiceOption
|
||||||
|
{
|
||||||
|
/** @var \Pterodactyl\Models\ServiceOption $instance */
|
||||||
|
$instance = $this->getBuilder()->with('scriptFrom', 'configFrom', 'variables')->find($id, $this->getColumns());
|
||||||
|
if (! $instance) {
|
||||||
|
throw new RecordNotFoundException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirm a copy script belongs to the same service as the item trying to use it.
|
* Confirm a copy script belongs to the same service as the item trying to use it.
|
||||||
*
|
*
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
namespace Pterodactyl\Repositories\Eloquent;
|
namespace Pterodactyl\Repositories\Eloquent;
|
||||||
|
|
||||||
use Pterodactyl\Models\Service;
|
use Pterodactyl\Models\Service;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||||
use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface;
|
use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface;
|
||||||
|
|
||||||
|
@ -25,9 +24,14 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* Return a service or all services with their associated options, variables, and packs.
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @return \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Service
|
||||||
|
*
|
||||||
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
*/
|
*/
|
||||||
public function getWithOptions(int $id = null): Collection
|
public function getWithOptions(int $id = null)
|
||||||
{
|
{
|
||||||
$instance = $this->getBuilder()->with('options.packs', 'options.variables');
|
$instance = $this->getBuilder()->with('options.packs', 'options.variables');
|
||||||
|
|
||||||
|
@ -44,9 +48,14 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* Return a service or all services and the count of options, packs, and servers for that service.
|
||||||
|
*
|
||||||
|
* @param int|null $id
|
||||||
|
* @return \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Service
|
||||||
|
*
|
||||||
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
*/
|
*/
|
||||||
public function getWithCounts(int $id = null): Collection
|
public function getWithCounts(int $id = null)
|
||||||
{
|
{
|
||||||
$instance = $this->getBuilder()->withCount(['options', 'packs', 'servers']);
|
$instance = $this->getBuilder()->withCount(['options', 'packs', 'servers']);
|
||||||
|
|
||||||
|
@ -63,7 +72,12 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* Return a service along with its associated options and the servers relation on those options.
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @return \Pterodactyl\Models\Service
|
||||||
|
*
|
||||||
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
*/
|
*/
|
||||||
public function getWithOptionServers(int $id): Service
|
public function getWithOptionServers(int $id): Service
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,132 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Pterodactyl - Panel
|
|
||||||
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
|
|
||||||
*
|
|
||||||
* This software is licensed under the terms of the MIT license.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Pterodactyl\Services\Services\Exporter;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Sabre\Xml\Writer;
|
|
||||||
use Sabre\Xml\Service;
|
|
||||||
use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface;
|
|
||||||
|
|
||||||
class XMLExporterService
|
|
||||||
{
|
|
||||||
const XML_OPTION_NAMESPACE = '{https://pterodactyl.io/exporter/option/}';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Carbon\Carbon
|
|
||||||
*/
|
|
||||||
protected $carbon;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface
|
|
||||||
*/
|
|
||||||
protected $repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Sabre\Xml\Service
|
|
||||||
*/
|
|
||||||
protected $xml;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* XMLExporterService constructor.
|
|
||||||
*
|
|
||||||
* @param \Carbon\Carbon $carbon
|
|
||||||
* @param \Sabre\Xml\Service $xml
|
|
||||||
* @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
Carbon $carbon,
|
|
||||||
Service $xml,
|
|
||||||
ServiceOptionRepositoryInterface $repository
|
|
||||||
) {
|
|
||||||
$this->carbon = $carbon;
|
|
||||||
$this->repository = $repository;
|
|
||||||
$this->xml = $xml;
|
|
||||||
|
|
||||||
$this->xml->namespaceMap = [
|
|
||||||
str_replace(['{', '}'], '', self::XML_OPTION_NAMESPACE) => 'p',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an XML structure to represent this service option.
|
|
||||||
*
|
|
||||||
* @param int $option
|
|
||||||
* @return string
|
|
||||||
*
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
|
||||||
*/
|
|
||||||
public function handle(int $option): string
|
|
||||||
{
|
|
||||||
$option = $this->repository->getWithCopyAttributes($option);
|
|
||||||
|
|
||||||
$struct = [
|
|
||||||
'meta' => [
|
|
||||||
'version' => 'PTDL_v1',
|
|
||||||
],
|
|
||||||
'exported_at' => $this->carbon->now()->toIso8601String(),
|
|
||||||
'name' => $option->name,
|
|
||||||
'author' => array_get(explode(':', $option->tag), 0),
|
|
||||||
'tag' => $option->tag,
|
|
||||||
'description' => $this->writeCData($option->description),
|
|
||||||
'image' => $option->docker_image,
|
|
||||||
'config' => [
|
|
||||||
'files' => $this->writeCData($option->inherit_config_files),
|
|
||||||
'startup' => $this->writeCData($option->inherit_config_startup),
|
|
||||||
'logs' => $this->writeCData($option->inherit_config_logs),
|
|
||||||
'stop' => $option->inherit_config_stop,
|
|
||||||
],
|
|
||||||
'scripts' => [
|
|
||||||
'installation' => [
|
|
||||||
'script' => $this->writeCData($option->copy_script_install),
|
|
||||||
'container' => $option->copy_script_container,
|
|
||||||
'entrypoint' => $option->copy_script_entry,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
return $this->xml->write(self::XML_OPTION_NAMESPACE . 'root', $this->recursiveArrayKeyPrepend($struct));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array $array
|
|
||||||
* @param string $prepend
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function recursiveArrayKeyPrepend(array $array, $prepend = self::XML_OPTION_NAMESPACE): array
|
|
||||||
{
|
|
||||||
$parsed = [];
|
|
||||||
foreach ($array as $k => &$v) {
|
|
||||||
$k = $prepend . $k;
|
|
||||||
|
|
||||||
if (is_array($v)) {
|
|
||||||
$v = $this->recursiveArrayKeyPrepend($v);
|
|
||||||
}
|
|
||||||
|
|
||||||
$parsed[$k] = $v;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a closure to be used by the XML writer to generate a string wrapped in CDATA tags.
|
|
||||||
*
|
|
||||||
* @param string $value
|
|
||||||
* @return \Closure
|
|
||||||
*/
|
|
||||||
protected function writeCData(string $value): Closure
|
|
||||||
{
|
|
||||||
return function (Writer $writer) use ($value) {
|
|
||||||
return $writer->writeCData($value);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Pterodactyl - Panel
|
||||||
|
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
|
||||||
|
*
|
||||||
|
* This software is licensed under the terms of the MIT license.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Pterodactyl\Services\Services\Sharing;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface;
|
||||||
|
|
||||||
|
class ServiceOptionExporterService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var \Carbon\Carbon
|
||||||
|
*/
|
||||||
|
protected $carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface
|
||||||
|
*/
|
||||||
|
protected $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XMLExporterService constructor.
|
||||||
|
*
|
||||||
|
* @param \Carbon\Carbon $carbon
|
||||||
|
* @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
Carbon $carbon,
|
||||||
|
ServiceOptionRepositoryInterface $repository
|
||||||
|
) {
|
||||||
|
$this->carbon = $carbon;
|
||||||
|
$this->repository = $repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an XML structure to represent this service option.
|
||||||
|
*
|
||||||
|
* @param int $option
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
|
*/
|
||||||
|
public function handle(int $option): string
|
||||||
|
{
|
||||||
|
$option = $this->repository->getWithExportAttributes($option);
|
||||||
|
|
||||||
|
$struct = [
|
||||||
|
'_comment' => 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO',
|
||||||
|
'meta' => [
|
||||||
|
'version' => 'PTDL_v1',
|
||||||
|
],
|
||||||
|
'exported_at' => $this->carbon->now()->toIso8601String(),
|
||||||
|
'name' => $option->name,
|
||||||
|
'author' => array_get(explode(':', $option->tag), 0),
|
||||||
|
'tag' => $option->tag,
|
||||||
|
'description' => $option->description,
|
||||||
|
'image' => $option->docker_image,
|
||||||
|
'startup' => $option->display_startup,
|
||||||
|
'config' => [
|
||||||
|
'files' => $option->inherit_config_files,
|
||||||
|
'startup' => $option->inherit_config_startup,
|
||||||
|
'logs' => $option->inherit_config_logs,
|
||||||
|
'stop' => $option->inherit_config_stop,
|
||||||
|
],
|
||||||
|
'scripts' => [
|
||||||
|
'installation' => [
|
||||||
|
'script' => $option->copy_script_install,
|
||||||
|
'container' => $option->copy_script_container,
|
||||||
|
'entrypoint' => $option->copy_script_entry,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'variables' => $option->variables->transform(function ($item) {
|
||||||
|
return collect($item->toArray())->except([
|
||||||
|
'id', 'option_id', 'created_at', 'updated_at',
|
||||||
|
])->toArray();
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
return json_encode($struct, JSON_PRETTY_PRINT);
|
||||||
|
}
|
||||||
|
}
|
123
app/Services/Services/Sharing/ServiceOptionImporterService.php
Normal file
123
app/Services/Services/Sharing/ServiceOptionImporterService.php
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Pterodactyl - Panel
|
||||||
|
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
|
||||||
|
*
|
||||||
|
* This software is licensed under the terms of the MIT license.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Pterodactyl\Services\Services\Sharing;
|
||||||
|
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Pterodactyl\Models\ServiceOption;
|
||||||
|
use Illuminate\Database\ConnectionInterface;
|
||||||
|
use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface;
|
||||||
|
use Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException;
|
||||||
|
use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface;
|
||||||
|
use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface;
|
||||||
|
use Pterodactyl\Exceptions\Service\ServiceOption\DuplicateOptionTagException;
|
||||||
|
|
||||||
|
class ServiceOptionImporterService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var \Illuminate\Database\ConnectionInterface
|
||||||
|
*/
|
||||||
|
protected $connection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface
|
||||||
|
*/
|
||||||
|
protected $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface
|
||||||
|
*/
|
||||||
|
protected $serviceRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface
|
||||||
|
*/
|
||||||
|
protected $serviceVariableRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XMLImporterService constructor.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||||
|
* @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository
|
||||||
|
* @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository
|
||||||
|
* @param \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface $serviceVariableRepository
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
ConnectionInterface $connection,
|
||||||
|
ServiceRepositoryInterface $serviceRepository,
|
||||||
|
ServiceOptionRepositoryInterface $repository,
|
||||||
|
ServiceVariableRepositoryInterface $serviceVariableRepository
|
||||||
|
) {
|
||||||
|
$this->connection = $connection;
|
||||||
|
$this->repository = $repository;
|
||||||
|
$this->serviceRepository = $serviceRepository;
|
||||||
|
$this->serviceVariableRepository = $serviceVariableRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take an uploaded XML file and parse it into a new service option.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\UploadedFile $file
|
||||||
|
* @param int $service
|
||||||
|
* @return \Pterodactyl\Models\ServiceOption
|
||||||
|
*
|
||||||
|
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||||
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
|
* @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException
|
||||||
|
*/
|
||||||
|
public function handle(UploadedFile $file, int $service): ServiceOption
|
||||||
|
{
|
||||||
|
if (! $file->isValid() || ! $file->isFile()) {
|
||||||
|
throw new InvalidFileUploadException(trans('exceptions.service.exporter.import_file_error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsed = json_decode($file->openFile()->fread($file->getSize()));
|
||||||
|
|
||||||
|
if (object_get($parsed, 'meta.version') !== 'PTDL_v1') {
|
||||||
|
throw new InvalidFileUploadException(trans('exceptions.service.exporter.invalid_json_provided'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$service = $this->serviceRepository->getWithOptions($service);
|
||||||
|
$service->options->each(function ($option) use ($parsed) {
|
||||||
|
if ($option->tag === object_get($parsed, 'tag')) {
|
||||||
|
throw new DuplicateOptionTagException(trans('exceptions.service.options.duplicate_tag'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->connection->beginTransaction();
|
||||||
|
$option = $this->repository->create([
|
||||||
|
'uuid' => Uuid::uuid4()->toString(),
|
||||||
|
'service_id' => $service->id,
|
||||||
|
'name' => object_get($parsed, 'name'),
|
||||||
|
'description' => object_get($parsed, 'description'),
|
||||||
|
'tag' => object_get($parsed, 'tag'),
|
||||||
|
'docker_image' => object_get($parsed, 'image'),
|
||||||
|
'config_files' => object_get($parsed, 'config.files'),
|
||||||
|
'config_startup' => object_get($parsed, 'config.startup'),
|
||||||
|
'config_logs' => object_get($parsed, 'config.logs'),
|
||||||
|
'config_stop' => object_get($parsed, 'config.stop'),
|
||||||
|
'startup' => object_get($parsed, 'startup'),
|
||||||
|
'script_install' => object_get($parsed, 'scripts.installation.script'),
|
||||||
|
'script_entry' => object_get($parsed, 'scripts.installation.entrypoint'),
|
||||||
|
'script_container' => object_get($parsed, 'scripts.installation.container'),
|
||||||
|
'copy_script_from' => null,
|
||||||
|
], true, true);
|
||||||
|
|
||||||
|
collect($parsed->variables)->each(function ($variable) use ($option) {
|
||||||
|
$this->serviceVariableRepository->create(array_merge((array) $variable, [
|
||||||
|
'option_id' => $option->id,
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->connection->commit();
|
||||||
|
|
||||||
|
return $option;
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,7 +35,6 @@
|
||||||
"prologue/alerts": "^0.4",
|
"prologue/alerts": "^0.4",
|
||||||
"ramsey/uuid": "^3.7",
|
"ramsey/uuid": "^3.7",
|
||||||
"s1lentium/iptools": "^1.1",
|
"s1lentium/iptools": "^1.1",
|
||||||
"sabre/xml": "^2.0",
|
|
||||||
"sofa/eloquence": "~5.4.1",
|
"sofa/eloquence": "~5.4.1",
|
||||||
"spatie/laravel-fractal": "^4.0",
|
"spatie/laravel-fractal": "^4.0",
|
||||||
"watson/validating": "^3.0",
|
"watson/validating": "^3.0",
|
||||||
|
|
116
composer.lock
generated
116
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "bc8c88f86ea043406bce2f8fddf704b3",
|
"content-hash": "46a0a06ec8f3af50ed6ec05c2bb3b9a3",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-sdk-php",
|
"name": "aws/aws-sdk-php",
|
||||||
|
@ -2510,120 +2510,6 @@
|
||||||
],
|
],
|
||||||
"time": "2016-08-21T15:57:09+00:00"
|
"time": "2016-08-21T15:57:09+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "sabre/uri",
|
|
||||||
"version": "2.1.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/fruux/sabre-uri.git",
|
|
||||||
"reference": "a42126042c7dcb53e2978dadb6d22574d1359b4c"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/fruux/sabre-uri/zipball/a42126042c7dcb53e2978dadb6d22574d1359b4c",
|
|
||||||
"reference": "a42126042c7dcb53e2978dadb6d22574d1359b4c",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=7"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "^6.0",
|
|
||||||
"sabre/cs": "~1.0.0"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"files": [
|
|
||||||
"lib/functions.php"
|
|
||||||
],
|
|
||||||
"psr-4": {
|
|
||||||
"Sabre\\Uri\\": "lib/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"BSD-3-Clause"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Evert Pot",
|
|
||||||
"email": "me@evertpot.com",
|
|
||||||
"homepage": "http://evertpot.com/",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Functions for making sense out of URIs.",
|
|
||||||
"homepage": "http://sabre.io/uri/",
|
|
||||||
"keywords": [
|
|
||||||
"rfc3986",
|
|
||||||
"uri",
|
|
||||||
"url"
|
|
||||||
],
|
|
||||||
"time": "2017-02-20T20:02:35+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "sabre/xml",
|
|
||||||
"version": "2.0.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/fruux/sabre-xml.git",
|
|
||||||
"reference": "054292959a1f2b64c10c9c7a03a816ba1872b8a3"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/fruux/sabre-xml/zipball/054292959a1f2b64c10c9c7a03a816ba1872b8a3",
|
|
||||||
"reference": "054292959a1f2b64c10c9c7a03a816ba1872b8a3",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-dom": "*",
|
|
||||||
"ext-xmlreader": "*",
|
|
||||||
"ext-xmlwriter": "*",
|
|
||||||
"lib-libxml": ">=2.6.20",
|
|
||||||
"php": ">=7.0",
|
|
||||||
"sabre/uri": ">=1.0,<3.0.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "*",
|
|
||||||
"sabre/cs": "~1.0.0"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Sabre\\Xml\\": "lib/"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"lib/Deserializer/functions.php",
|
|
||||||
"lib/Serializer/functions.php"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"BSD-3-Clause"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Evert Pot",
|
|
||||||
"email": "me@evertpot.com",
|
|
||||||
"homepage": "http://evertpot.com/",
|
|
||||||
"role": "Developer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Markus Staab",
|
|
||||||
"email": "markus.staab@redaxo.de",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "sabre/xml is an XML library that you may not hate.",
|
|
||||||
"homepage": "https://sabre.io/xml/",
|
|
||||||
"keywords": [
|
|
||||||
"XMLReader",
|
|
||||||
"XMLWriter",
|
|
||||||
"dom",
|
|
||||||
"xml"
|
|
||||||
],
|
|
||||||
"time": "2016-11-16T00:41:01+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "sofa/eloquence",
|
"name": "sofa/eloquence",
|
||||||
"version": "5.4.1",
|
"version": "5.4.1",
|
||||||
|
|
|
@ -21,6 +21,7 @@ return [
|
||||||
'service' => [
|
'service' => [
|
||||||
'delete_has_servers' => 'A service with active servers attached to it cannot be deleted from the Panel.',
|
'delete_has_servers' => 'A service with active servers attached to it cannot be deleted from the Panel.',
|
||||||
'options' => [
|
'options' => [
|
||||||
|
'duplicate_tag' => 'A service option with that tag already exists for this service.',
|
||||||
'delete_has_servers' => 'A service option with active servers attached to it cannot be deleted from the Panel.',
|
'delete_has_servers' => 'A service option with active servers attached to it cannot be deleted from the Panel.',
|
||||||
'invalid_copy_id' => 'The service option selected for copying a script from either does not exist, or is copying a script itself.',
|
'invalid_copy_id' => 'The service option selected for copying a script from either does not exist, or is copying a script itself.',
|
||||||
'must_be_child' => 'The "Copy Settings From" directive for this option must be a child option for the selected service.',
|
'must_be_child' => 'The "Copy Settings From" directive for this option must be a child option for the selected service.',
|
||||||
|
@ -30,6 +31,10 @@ return [
|
||||||
'env_not_unique' => 'The environment variable :name must be unique to this service option.',
|
'env_not_unique' => 'The environment variable :name must be unique to this service option.',
|
||||||
'reserved_name' => 'The environment variable :name is protected and cannot be assigned to a variable.',
|
'reserved_name' => 'The environment variable :name is protected and cannot be assigned to a variable.',
|
||||||
],
|
],
|
||||||
|
'exporter' => [
|
||||||
|
'import_file_error' => 'The XML file provided was not valid.',
|
||||||
|
'invalid_json_provided' => 'The JSON file provided is not in a format that can be recognized.',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'packs' => [
|
'packs' => [
|
||||||
'delete_has_servers' => 'Cannot delete a pack that is attached to active servers.',
|
'delete_has_servers' => 'Cannot delete a pack that is attached to active servers.',
|
||||||
|
|
|
@ -31,7 +31,8 @@
|
||||||
<div class="box-header with-border">
|
<div class="box-header with-border">
|
||||||
<h3 class="box-title">Configured Service</h3>
|
<h3 class="box-title">Configured Service</h3>
|
||||||
<div class="box-tools">
|
<div class="box-tools">
|
||||||
<a href="{{ route('admin.services.new') }}"><button class="btn btn-primary btn-sm">Create New</button></a>
|
<a href="#" class="btn btn-sm btn-success" data-toggle="modal" data-target="#importServiceOptionModal" role="button"><i class="fa fa-upload"></i> Import Service Option</a>
|
||||||
|
<a href="{{ route('admin.services.new') }}" class="btn btn-primary btn-sm">Create New</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-body table-responsive no-padding">
|
<div class="box-body table-responsive no-padding">
|
||||||
|
@ -57,4 +58,50 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal fade" tabindex="-1" role="dialog" id="importServiceOptionModal">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
<h4 class="modal-title">Import a Service Option</h4>
|
||||||
|
</div>
|
||||||
|
<form action="{{ route('admin.services.option.import') }}" enctype="multipart/form-data" method="POST">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label" for="pImportFile">Service File <span class="field-required"></span></label>
|
||||||
|
<div>
|
||||||
|
<input id="pImportFile" type="file" name="import_file" class="form-control" accept="application/json" />
|
||||||
|
<p class="small text-muted">Select the <code>.json</code> file for the new service option that you wish to import.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label" for="pImportToService">Associated Service <span class="field-required"></span></label>
|
||||||
|
<div>
|
||||||
|
<select id="pImportToService" name="import_to_service">
|
||||||
|
@foreach($services as $service)
|
||||||
|
<option value="{{ $service->id }}">{{ $service->name }} <{{ $service->author }}></option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<p class="small text-muted">Select the service that this option will be associated with from the dropdown. If you wish to associate it with a new service you will need to create that service before continuing.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
{{ csrf_field() }}
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Import</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('footer-scripts')
|
||||||
|
@parent
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#pImportToService').select2();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
|
@ -160,6 +160,7 @@ Route::group(['prefix' => 'services'], function () {
|
||||||
Route::get('/option/{option}/scripts', 'OptionController@viewScripts')->name('admin.services.option.scripts');
|
Route::get('/option/{option}/scripts', 'OptionController@viewScripts')->name('admin.services.option.scripts');
|
||||||
|
|
||||||
Route::post('/new', 'ServiceController@store');
|
Route::post('/new', 'ServiceController@store');
|
||||||
|
Route::post('/import', 'Services\Options\OptionShareController@import')->name('admin.services.option.import');
|
||||||
Route::post('/option/new', 'OptionController@store');
|
Route::post('/option/new', 'OptionController@store');
|
||||||
Route::post('/option/{option}/variables', 'VariableController@store');
|
Route::post('/option/{option}/variables', 'VariableController@store');
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue