Merge branch 'develop' into develop
This commit is contained in:
commit
ea778e9345
159 changed files with 3400 additions and 3896 deletions
47
CHANGELOG.md
47
CHANGELOG.md
|
@ -3,6 +3,49 @@ This file is a running track of new features and fixes to each version of the pa
|
|||
|
||||
This project follows [Semantic Versioning](http://semver.org) guidelines.
|
||||
|
||||
## v1.0.0
|
||||
Pterodactyl 1.0 represents the culmination of over two years of work, almost 2,000 commits, endless bug and feature requests, and a dream that
|
||||
has been in the making since 2013. 🎉
|
||||
|
||||
Due to the sheer size and timeline of this release I've massively truncated the listing below. There are numerous smaller
|
||||
bug fixes and changes that would simply be too difficult to keep track of here. Please feel free to browse through the releases
|
||||
tab for this repository to see more specific changes that have been made.
|
||||
|
||||
### Added
|
||||
* Adds a new client-facing API allowing a user to control all aspects of their individual servers, or servers
|
||||
which they have been granted access to as a subuser.
|
||||
* Adds the ability for backups to be created for a server both manually and via a scheduled task.
|
||||
* Adds the ability for users to modify their server allocations on the fly and include notes for each allocation.
|
||||
* Adds the ability for users to generate recovery tokens for 2FA protected logins which can be used in place of
|
||||
a code should their device be inaccessible.
|
||||
* Adds support for transfering servers between Nodes via the Panel.
|
||||
* Adds the ability to assign specific CPU cores to a server (CPU Pinning) process.
|
||||
* Server owners can now reinstall their assigned server egg automatically with a button on the frontend.
|
||||
|
||||
### Changed
|
||||
* The entire user frontend has been replaced with a responsive, React backed design implemented using Tailwind CSS.
|
||||
* Replaces a large amount of complex daemon authentication logic by funneling most API calls through the Panel, and using
|
||||
JSON Web Tokens where necessary to handle one-time direct authentication with Wings.
|
||||
* Frontend server listing now includes a toggle to show or hide servers which an administrator has access to, rather
|
||||
than always showing all servers on the system when logged into an admin account.
|
||||
* We've replaced Ace Editor on the frontend with a better solution to allow lighter builds and more end-user functionality.
|
||||
* Server permissions have been overhauled to be both easier to understand in the codebase, and allows plugins to better
|
||||
hook into the permission system.
|
||||
|
||||
### Removed
|
||||
* Removes large swaths of code complexity and confusing interface designs that caused a lot of pain to new developers
|
||||
trying to jump into the codebase. We've simplified this to stick to more established Laravel design standards to make
|
||||
it easy to parse through the project and make contributions.
|
||||
|
||||
## v0.7.19 (Derelict Dermodactylus)
|
||||
### Fixed
|
||||
* **[Security]** Fixes XSS in the admin area's server owner selection.
|
||||
|
||||
## v0.7.18 (Derelict Dermodactylus)
|
||||
### Fixed
|
||||
* **[Security]** Re-addressed missed endpoint that would not properly limit a user account to 5 API keys.
|
||||
* **[Security]** Addresses a Client API vulnerability that would allow a user to list all servers on the system ([`GHSA-6888-7f3w-92jx`](https://github.com/pterodactyl/panel/security/advisories/GHSA-6888-7f3w-92jx))
|
||||
|
||||
## v0.7.17 (Derelict Dermodactylus)
|
||||
### Fixed
|
||||
* Limited accounts to 5 API keys at a time.
|
||||
|
@ -301,7 +344,7 @@ the response from the server `GET` endpoint.
|
|||
* Nest and Egg listings now show the associated ID in order to make API requests easier.
|
||||
* Added star indicators to user listing in Admin CP to indicate users who are set as a root admin.
|
||||
* Creating a new node will now requires a SSL connection if the Panel is configured to use SSL as well.
|
||||
* Connector error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure.
|
||||
* Socketio error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure.
|
||||
* File manager now supports mass deletion option for files and folders.
|
||||
* Support for CS:GO as a default service option selection.
|
||||
* Support for GMOD as a default service option selection.
|
||||
|
@ -431,7 +474,7 @@ the response from the server `GET` endpoint.
|
|||
* Changed 2FA login process to be more secure. Previously authentication checking happened on the 2FA post page, now it happens prior and is passed along to the 2FA page to avoid storing any credentials.
|
||||
|
||||
### Added
|
||||
* Connector error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure.
|
||||
* Socketio error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure.
|
||||
|
||||
## v0.7.0-beta.1 (Derelict Dermodactylus)
|
||||
### Added
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
[![Logo Image](https://cdn.pterodactyl.io/logos/new/pterodactyl_logo.png)](https://pterodactyl.io)
|
||||
|
||||
[![Build status](https://img.shields.io/travis/pterodactyl/panel/develop.svg?style=flat-square)](https://travis-ci.org/pterodactyl/panel)
|
||||
[![Codecov](https://img.shields.io/codecov/c/github/pterodactyl/panel/develop.svg?style=flat-square)](https://codecov.io/gh/Pterodactyl/Panel)
|
||||
[![Discord](https://img.shields.io/discord/122900397965705216.svg?style=flat-square&label=Discord)](https://pterodactyl.io/discord)
|
||||
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/pterodactyl/panel/tests?label=Tests&style=for-the-badge)
|
||||
![Discord](https://img.shields.io/discord/122900397965705216?label=Discord&logo=Discord&logoColor=white&style=for-the-badge)
|
||||
![GitHub Releases](https://img.shields.io/github/downloads/pterodactyl/panel/latest/total?style=for-the-badge)
|
||||
![GitHub Pre-Releases](https://img.shields.io/github/downloads-pre/pterodactyl/panel/v1.0.0-rc.7/total?style=for-the-badge)
|
||||
![GitHub contributors](https://img.shields.io/github/contributors/pterodactyl/panel?style=for-the-badge)
|
||||
|
||||
# Pterodactyl Panel
|
||||
Pterodactyl is an open-source game server management panel built with PHP 7, React, and Go. Designed with security
|
||||
|
|
26
app/Console/Commands/Overrides/SeedCommand.php
Normal file
26
app/Console/Commands/Overrides/SeedCommand.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Console\Commands\Overrides;
|
||||
|
||||
use Pterodactyl\Console\RequiresDatabaseMigrations;
|
||||
use Illuminate\Database\Console\Seeds\SeedCommand as BaseSeedCommand;
|
||||
|
||||
class SeedCommand extends BaseSeedCommand
|
||||
{
|
||||
use RequiresDatabaseMigrations;
|
||||
|
||||
/**
|
||||
* Block someone from running this seed command if they have not completed the migration
|
||||
* process.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (!$this->hasCompletedMigrations()) {
|
||||
return $this->showMigrationWarning();
|
||||
}
|
||||
|
||||
return parent::handle();
|
||||
}
|
||||
}
|
23
app/Console/Commands/Overrides/UpCommand.php
Normal file
23
app/Console/Commands/Overrides/UpCommand.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Console\Commands\Overrides;
|
||||
|
||||
use Pterodactyl\Console\RequiresDatabaseMigrations;
|
||||
use Illuminate\Foundation\Console\UpCommand as BaseUpCommand;
|
||||
|
||||
class UpCommand extends BaseUpCommand
|
||||
{
|
||||
use RequiresDatabaseMigrations;
|
||||
|
||||
/**
|
||||
* @return bool|int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (!$this->hasCompletedMigrations()) {
|
||||
return $this->showMigrationWarning();
|
||||
}
|
||||
|
||||
return parent::handle();
|
||||
}
|
||||
}
|
61
app/Console/RequiresDatabaseMigrations.php
Normal file
61
app/Console/RequiresDatabaseMigrations.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Console;
|
||||
|
||||
/**
|
||||
* @mixin \Illuminate\Console\Command
|
||||
*/
|
||||
trait RequiresDatabaseMigrations
|
||||
{
|
||||
/**
|
||||
* Checks if the migrations have finished running by comparing the last migration file.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasCompletedMigrations(): bool
|
||||
{
|
||||
/** @var \Illuminate\Database\Migrations\Migrator $migrator */
|
||||
$migrator = $this->getLaravel()->make('migrator');
|
||||
|
||||
$files = $migrator->getMigrationFiles(database_path('migrations'));
|
||||
|
||||
if (! $migrator->repositoryExists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (array_diff(array_keys($files), $migrator->getRepository()->getRan())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw a massive error into the console to hopefully catch the users attention and get
|
||||
* them to properly run the migrations rather than ignoring all of the other previous
|
||||
* errors...
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function showMigrationWarning(): int
|
||||
{
|
||||
$this->getOutput()->writeln("<options=bold>
|
||||
| @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
|
||||
| |
|
||||
| Your database has not been properly migrated! |
|
||||
| |
|
||||
| @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |</>
|
||||
|
||||
You must run the following command to finish migrating your database:
|
||||
|
||||
<fg=green;options=bold>php artisan migrate --step --force</>
|
||||
|
||||
You will not be able to use Pterodactyl Panel as expected without fixing your
|
||||
database state by running the command above.
|
||||
");
|
||||
|
||||
$this->getOutput()->error("You must correct the error above before continuing.");
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
|
@ -42,18 +42,6 @@ interface DatabaseRepositoryInterface extends RepositoryInterface
|
|||
*/
|
||||
public function getDatabasesForHost(int $host, int $count = 25): LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* Create a new database if it does not already exist on the host with
|
||||
* the provided details.
|
||||
*
|
||||
* @param array $data
|
||||
* @return \Pterodactyl\Models\Database
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException
|
||||
*/
|
||||
public function createIfNotExists(array $data): Database;
|
||||
|
||||
/**
|
||||
* Create a new database on a given connection.
|
||||
*
|
||||
|
|
|
@ -54,15 +54,4 @@ interface NodeRepositoryInterface extends RepositoryInterface
|
|||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function getNodesForServerCreation(): Collection;
|
||||
|
||||
/**
|
||||
* Return the IDs of all nodes that exist in the provided locations and have the space
|
||||
* available to support the additional disk and memory provided.
|
||||
*
|
||||
* @param array $locations
|
||||
* @param int $disk
|
||||
* @param int $memory
|
||||
* @return \Illuminate\Support\LazyCollection
|
||||
*/
|
||||
public function getNodesWithResourceUse(array $locations, int $disk, int $memory): LazyCollection;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
|
@ -217,7 +218,9 @@ class Handler extends ExceptionHandler
|
|||
'status' => method_exists($exception, 'getStatusCode')
|
||||
? strval($exception->getStatusCode())
|
||||
: ($exception instanceof ValidationException ? '422' : '500'),
|
||||
'detail' => 'An error was encountered while processing this request.',
|
||||
'detail' => $exception instanceof HttpExceptionInterface
|
||||
? $exception->getMessage()
|
||||
: 'An unexpected error was encountered while processing this request, please try again.',
|
||||
];
|
||||
|
||||
if ($exception instanceof ModelNotFoundException || $exception->getPrevious() instanceof ModelNotFoundException) {
|
||||
|
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
namespace Pterodactyl\Http\Controllers\Admin;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Nest;
|
||||
use Pterodactyl\Models\Mount;
|
||||
use Pterodactyl\Models\Location;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Services\Mounts\MountUpdateService;
|
||||
use Pterodactyl\Http\Requests\Admin\MountFormRequest;
|
||||
|
@ -37,21 +39,6 @@ class MountController extends Controller
|
|||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Mounts\MountCreationService
|
||||
*/
|
||||
protected $creationService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Mounts\MountDeletionService
|
||||
*/
|
||||
protected $deletionService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Mounts\MountUpdateService
|
||||
*/
|
||||
protected $updateService;
|
||||
|
||||
/**
|
||||
* MountController constructor.
|
||||
*
|
||||
|
@ -59,26 +46,17 @@ class MountController extends Controller
|
|||
* @param \Pterodactyl\Contracts\Repository\NestRepositoryInterface $nestRepository
|
||||
* @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\MountRepository $repository
|
||||
* @param \Pterodactyl\Services\Mounts\MountCreationService $creationService
|
||||
* @param \Pterodactyl\Services\Mounts\MountDeletionService $deletionService
|
||||
* @param \Pterodactyl\Services\Mounts\MountUpdateService $updateService
|
||||
*/
|
||||
public function __construct(
|
||||
AlertsMessageBag $alert,
|
||||
NestRepositoryInterface $nestRepository,
|
||||
LocationRepositoryInterface $locationRepository,
|
||||
MountRepository $repository,
|
||||
MountCreationService $creationService,
|
||||
MountDeletionService $deletionService,
|
||||
MountUpdateService $updateService
|
||||
MountRepository $repository
|
||||
) {
|
||||
$this->alert = $alert;
|
||||
$this->nestRepository = $nestRepository;
|
||||
$this->locationRepository = $locationRepository;
|
||||
$this->repository = $repository;
|
||||
$this->creationService = $creationService;
|
||||
$this->deletionService = $deletionService;
|
||||
$this->updateService = $updateService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -103,11 +81,8 @@ class MountController extends Controller
|
|||
*/
|
||||
public function view($id)
|
||||
{
|
||||
$nests = $this->nestRepository->all();
|
||||
$nests->load('eggs');
|
||||
|
||||
$locations = $this->locationRepository->all();
|
||||
$locations->load('nodes');
|
||||
$nests = Nest::query()->with('eggs')->get();
|
||||
$locations = Location::query()->with('nodes')->get();
|
||||
|
||||
return view('admin.mounts.view', [
|
||||
'mount' => $this->repository->getWithRelations($id),
|
||||
|
@ -126,7 +101,13 @@ class MountController extends Controller
|
|||
*/
|
||||
public function create(MountFormRequest $request)
|
||||
{
|
||||
$mount = $this->creationService->handle($request->normalize());
|
||||
/** @var \Pterodactyl\Models\Mount $mount */
|
||||
$model = (new Mount())->fill($request->validated());
|
||||
$model->forceFill(['uuid' => Uuid::uuid4()->toString()]);
|
||||
|
||||
$model->saveOrFail();
|
||||
$mount = $model->fresh();
|
||||
|
||||
$this->alert->success('Mount was created successfully.')->flash();
|
||||
|
||||
return redirect()->route('admin.mounts.view', $mount->id);
|
||||
|
@ -147,7 +128,8 @@ class MountController extends Controller
|
|||
return $this->delete($mount);
|
||||
}
|
||||
|
||||
$this->updateService->handle($mount->id, $request->normalize());
|
||||
$mount->forceFill($request->validated())->save();
|
||||
|
||||
$this->alert->success('Mount was updated successfully.')->flash();
|
||||
|
||||
return redirect()->route('admin.mounts.view', $mount->id);
|
||||
|
@ -163,15 +145,9 @@ class MountController extends Controller
|
|||
*/
|
||||
public function delete(Mount $mount)
|
||||
{
|
||||
try {
|
||||
$this->deletionService->handle($mount->id);
|
||||
$mount->delete();
|
||||
|
||||
return redirect()->route('admin.mounts');
|
||||
} catch (DisplayException $ex) {
|
||||
$this->alert->danger($ex->getMessage())->flash();
|
||||
}
|
||||
|
||||
return redirect()->route('admin.mounts.view', $mount->id);
|
||||
return redirect()->route('admin.mounts');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -188,11 +164,12 @@ class MountController extends Controller
|
|||
]);
|
||||
|
||||
$eggs = $validatedData['eggs'] ?? [];
|
||||
if (sizeof($eggs) > 0) {
|
||||
$mount->eggs()->attach(array_map('intval', $eggs));
|
||||
$this->alert->success('Mount was updated successfully.')->flash();
|
||||
if (count($eggs) > 0) {
|
||||
$mount->eggs()->attach($eggs);
|
||||
}
|
||||
|
||||
$this->alert->success('Mount was updated successfully.')->flash();
|
||||
|
||||
return redirect()->route('admin.mounts.view', $mount->id);
|
||||
}
|
||||
|
||||
|
@ -205,16 +182,15 @@ class MountController extends Controller
|
|||
*/
|
||||
public function addNodes(Request $request, Mount $mount)
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'nodes' => 'required|exists:nodes,id',
|
||||
]);
|
||||
$data = $request->validate(['nodes' => 'required|exists:nodes,id']);
|
||||
|
||||
$nodes = $validatedData['nodes'] ?? [];
|
||||
if (sizeof($nodes) > 0) {
|
||||
$mount->nodes()->attach(array_map('intval', $nodes));
|
||||
$this->alert->success('Mount was updated successfully.')->flash();
|
||||
$nodes = $data['nodes'] ?? [];
|
||||
if (count($nodes) > 0) {
|
||||
$mount->nodes()->attach($nodes);
|
||||
}
|
||||
|
||||
$this->alert->success('Mount was updated successfully.')->flash();
|
||||
|
||||
return redirect()->route('admin.mounts.view', $mount->id);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
<?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\Controllers\Admin\Nests;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Pterodactyl\Models\Egg;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
|
@ -81,14 +75,14 @@ class EggScriptController extends Controller
|
|||
* Handle a request to update the installation script for an Egg.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Admin\Egg\EggScriptFormRequest $request
|
||||
* @param int $egg
|
||||
* @param \Pterodactyl\Models\Egg $egg
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @throws \Pterodactyl\Exceptions\Service\Egg\InvalidCopyFromException
|
||||
*/
|
||||
public function update(EggScriptFormRequest $request, int $egg): RedirectResponse
|
||||
public function update(EggScriptFormRequest $request, Egg $egg): RedirectResponse
|
||||
{
|
||||
$this->installScriptService->handle($egg, $request->normalize());
|
||||
$this->alert->success(trans('admin/nests.eggs.notices.script_updated'))->flash();
|
||||
|
|
|
@ -102,7 +102,7 @@ class EggShareController extends Controller
|
|||
* Update an existing Egg using a new imported file.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Admin\Egg\EggImportFormRequest $request
|
||||
* @param int $egg
|
||||
* @param \Pterodactyl\Models\Egg $egg
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
|
@ -110,7 +110,7 @@ class EggShareController extends Controller
|
|||
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
|
||||
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
|
||||
*/
|
||||
public function update(EggImportFormRequest $request, int $egg): RedirectResponse
|
||||
public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse
|
||||
{
|
||||
$this->updateImporterService->handle($egg, $request->file('import_file'));
|
||||
$this->alert->success(trans('admin/nests.eggs.notices.updated_via_import'))->flash();
|
||||
|
|
|
@ -12,7 +12,9 @@ namespace Pterodactyl\Http\Controllers\Admin;
|
|||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\Mount;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\MountServer;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
|
@ -251,7 +253,7 @@ class ServersController extends Controller
|
|||
*/
|
||||
public function reinstallServer(Server $server)
|
||||
{
|
||||
$this->reinstallService->reinstall($server);
|
||||
$this->reinstallService->handle($server);
|
||||
$this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash();
|
||||
|
||||
return redirect()->route('admin.servers.view.manage', $server->id);
|
||||
|
@ -332,13 +334,18 @@ class ServersController extends Controller
|
|||
* @return \Illuminate\Http\RedirectResponse
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function saveStartup(Request $request, Server $server)
|
||||
{
|
||||
$this->startupModificationService->setUserLevel(User::USER_LEVEL_ADMIN);
|
||||
$this->startupModificationService->handle($server, $request->except('_token'));
|
||||
try {
|
||||
$this->startupModificationService
|
||||
->setUserLevel(User::USER_LEVEL_ADMIN)
|
||||
->handle($server, $request->except('_token'));
|
||||
} catch (DataValidationException $exception) {
|
||||
throw new ValidationException($exception->validator);
|
||||
}
|
||||
|
||||
$this->alert->success(trans('admin/server.alerts.startup_changed'))->flash();
|
||||
|
||||
return redirect()->route('admin.servers.view.startup', $server->id);
|
||||
|
@ -356,7 +363,7 @@ class ServersController extends Controller
|
|||
public function newDatabase(StoreServerDatabaseRequest $request, Server $server)
|
||||
{
|
||||
$this->databaseManagementService->create($server, [
|
||||
'database' => $request->input('database'),
|
||||
'database' => DatabaseManagementService::generateUniqueDatabaseName($request->input('database'), $server->id),
|
||||
'remote' => $request->input('remote'),
|
||||
'database_host_id' => $request->input('database_host_id'),
|
||||
'max_connections' => $request->input('max_connections'),
|
||||
|
@ -403,7 +410,7 @@ class ServersController extends Controller
|
|||
['id', '=', $database],
|
||||
]);
|
||||
|
||||
$this->databaseManagementService->delete($database->id);
|
||||
$this->databaseManagementService->delete($database);
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
@ -412,12 +419,17 @@ class ServersController extends Controller
|
|||
* Add a mount to a server.
|
||||
*
|
||||
* @param Server $server
|
||||
* @param int $mount_id
|
||||
* @param \Pterodactyl\Models\Mount $mount
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException|\Throwable
|
||||
*/
|
||||
public function addMount(Server $server, int $mount_id)
|
||||
public function addMount(Server $server, Mount $mount)
|
||||
{
|
||||
$server->mounts()->attach($mount_id);
|
||||
$mountServer = new MountServer;
|
||||
$mountServer->mount_id = $mount->id;
|
||||
$mountServer->server_id = $server->id;
|
||||
$mountServer->saveOrFail();
|
||||
|
||||
$data = $this->serverConfigurationStructureService->handle($server);
|
||||
|
||||
|
@ -438,15 +450,15 @@ class ServersController extends Controller
|
|||
* Remove a mount from a server.
|
||||
*
|
||||
* @param Server $server
|
||||
* @param int $mount_id
|
||||
* @param \Pterodactyl\Models\Mount $mount
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function deleteMount(Server $server, int $mount_id)
|
||||
public function deleteMount(Server $server, Mount $mount)
|
||||
{
|
||||
$server->mounts()->detach($mount_id);
|
||||
MountServer::where('mount_id', $mount->id)->where('server_id', $server->id)->delete();
|
||||
|
||||
$data = $this->serverConfigurationStructureService->handle($server);
|
||||
|
||||
|
|
|
@ -86,8 +86,8 @@ class UserController extends Controller
|
|||
{
|
||||
$users = QueryBuilder::for(
|
||||
User::query()->select('users.*')
|
||||
->selectRaw('COUNT(subusers.id) as subuser_of_count')
|
||||
->selectRaw('COUNT(servers.id) as servers_count')
|
||||
->selectRaw('COUNT(DISTINCT(subusers.id)) as subuser_of_count')
|
||||
->selectRaw('COUNT(DISTINCT(servers.id)) as servers_count')
|
||||
->leftJoin('subusers', 'subusers.user_id', '=', 'users.id')
|
||||
->leftJoin('servers', 'servers.owner_id', '=', 'users.id')
|
||||
->groupBy('users.id')
|
||||
|
|
|
@ -110,7 +110,9 @@ class DatabaseController extends ApplicationApiController
|
|||
*/
|
||||
public function store(StoreServerDatabaseRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$database = $this->databaseManagementService->create($server, $request->validated());
|
||||
$database = $this->databaseManagementService->create($server, array_merge($request->validated(), [
|
||||
'database' => $request->databaseName(),
|
||||
]));
|
||||
|
||||
return $this->fractal->item($database)
|
||||
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
|
||||
|
@ -133,7 +135,7 @@ class DatabaseController extends ApplicationApiController
|
|||
*/
|
||||
public function delete(ServerDatabaseWriteRequest $request): Response
|
||||
{
|
||||
$this->databaseManagementService->delete($request->getModel(Database::class)->id);
|
||||
$this->databaseManagementService->delete($request->getModel(Database::class));
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ class ServerManagementController extends ApplicationApiController
|
|||
*/
|
||||
public function reinstall(ServerWriteRequest $request, Server $server): Response
|
||||
{
|
||||
$this->reinstallServerService->reinstall($server);
|
||||
$this->reinstallServerService->handle($server);
|
||||
|
||||
return $this->returnNoContent();
|
||||
}
|
||||
|
|
|
@ -129,7 +129,7 @@ class DatabaseController extends ClientApiController
|
|||
*/
|
||||
public function delete(DeleteDatabaseRequest $request, Server $server, Database $database): Response
|
||||
{
|
||||
$this->managementService->delete($database->id);
|
||||
$this->managementService->delete($database);
|
||||
|
||||
return Response::create('', Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ class FileController extends ClientApiController
|
|||
{
|
||||
$contents = $this->fileRepository
|
||||
->setServer($server)
|
||||
->getDirectory(urlencode($request->get('directory') ?? '/'));
|
||||
->getDirectory(urlencode(urldecode($request->get('directory') ?? '/')));
|
||||
|
||||
return $this->fractal->collection($contents)
|
||||
->transformWith($this->getTransformer(FileObjectTransformer::class))
|
||||
|
@ -91,7 +91,7 @@ class FileController extends ClientApiController
|
|||
{
|
||||
return new Response(
|
||||
$this->fileRepository->setServer($server)->getContent(
|
||||
urlencode($request->get('file')), config('pterodactyl.files.max_edit_size')
|
||||
urlencode(urldecode($request->get('file'))), config('pterodactyl.files.max_edit_size')
|
||||
),
|
||||
Response::HTTP_OK,
|
||||
['Content-Type' => 'text/plain']
|
||||
|
|
|
@ -120,15 +120,27 @@ class ScheduleController extends ClientApiController
|
|||
*/
|
||||
public function update(UpdateScheduleRequest $request, Server $server, Schedule $schedule)
|
||||
{
|
||||
$this->repository->update($schedule->id, [
|
||||
$active = (bool) $request->input('is_active');
|
||||
|
||||
$data = [
|
||||
'name' => $request->input('name'),
|
||||
'cron_day_of_week' => $request->input('day_of_week'),
|
||||
'cron_day_of_month' => $request->input('day_of_month'),
|
||||
'cron_hour' => $request->input('hour'),
|
||||
'cron_minute' => $request->input('minute'),
|
||||
'is_active' => (bool) $request->input('is_active'),
|
||||
'is_active' => $active,
|
||||
'next_run_at' => $this->getNextRunAt($request),
|
||||
]);
|
||||
];
|
||||
|
||||
// Toggle the processing state of the scheduled task when it is enabled or disabled so that an
|
||||
// invalid state can be reset without manual database intervention.
|
||||
//
|
||||
// @see https://github.com/pterodactyl/panel/issues/2425
|
||||
if ($schedule->is_active !== $active) {
|
||||
$data['is_processing'] = false;
|
||||
}
|
||||
|
||||
$this->repository->update($schedule->id, $data);
|
||||
|
||||
return $this->fractal->item($schedule->refresh())
|
||||
->transformWith($this->getTransformer(ScheduleTransformer::class))
|
||||
|
|
|
@ -69,7 +69,7 @@ class SettingsController extends ClientApiController
|
|||
*/
|
||||
public function reinstall(ReinstallServerRequest $request, Server $server)
|
||||
{
|
||||
$this->reinstallServerService->reinstall($server);
|
||||
$this->reinstallServerService->handle($server);
|
||||
|
||||
return new JsonResponse([], Response::HTTP_ACCEPTED);
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ class StartupController extends ClientApiController
|
|||
'server_id' => $server->id,
|
||||
'variable_id' => $variable->id,
|
||||
], [
|
||||
'variable_value' => $request->input('value'),
|
||||
'variable_value' => $request->input('value') ?? '',
|
||||
]);
|
||||
|
||||
$variable = $variable->refresh();
|
||||
|
|
|
@ -49,11 +49,11 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings
|
|||
return Database::query()->where('id', $id)->firstOrFail();
|
||||
});
|
||||
|
||||
$this->router->model('backup', Backup::class, function ($value) {
|
||||
$this->router->bind('backup', function ($value) {
|
||||
return Backup::query()->where('uuid', $value)->firstOrFail();
|
||||
});
|
||||
|
||||
$this->router->model('user', User::class, function ($value) {
|
||||
$this->router->bind('user', function ($value) {
|
||||
return User::query()->where('uuid', $value)->firstOrFail();
|
||||
});
|
||||
|
||||
|
|
|
@ -19,12 +19,12 @@ class EggFormRequest extends AdminFormRequest
|
|||
public function rules()
|
||||
{
|
||||
$rules = [
|
||||
'name' => 'required|string|max:255',
|
||||
'name' => 'required|string|max:191',
|
||||
'description' => 'nullable|string',
|
||||
'docker_image' => 'required|string|max:255',
|
||||
'docker_image' => 'required|string|max:191',
|
||||
'startup' => 'required|string',
|
||||
'config_from' => 'sometimes|bail|nullable|numeric',
|
||||
'config_stop' => 'required_without:config_from|nullable|string|max:255',
|
||||
'config_stop' => 'required_without:config_from|nullable|string|max:191',
|
||||
'config_startup' => 'required_without:config_from|nullable|json',
|
||||
'config_logs' => 'required_without:config_from|nullable|json',
|
||||
'config_files' => 'required_without:config_from|nullable|json',
|
||||
|
|
|
@ -15,9 +15,9 @@ class EggVariableFormRequest extends AdminFormRequest
|
|||
public function rules()
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|min:1|max:255',
|
||||
'name' => 'required|string|min:1|max:191',
|
||||
'description' => 'sometimes|nullable|string',
|
||||
'env_variable' => 'required|regex:/^[\w]{1,255}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES,
|
||||
'env_variable' => 'required|regex:/^[\w]{1,191}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES,
|
||||
'options' => 'sometimes|required|array',
|
||||
'rules' => 'bail|required|string',
|
||||
'default_value' => 'present',
|
||||
|
|
|
@ -1,11 +1,4 @@
|
|||
<?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;
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ class StoreNestFormRequest extends AdminFormRequest
|
|||
public function rules()
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|min:1|max:255',
|
||||
'name' => 'required|string|min:1|max:191',
|
||||
'description' => 'string|nullable',
|
||||
];
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ class AllocationFormRequest extends AdminFormRequest
|
|||
{
|
||||
return [
|
||||
'allocation_ip' => 'required|string',
|
||||
'allocation_alias' => 'sometimes|nullable|string|max:255',
|
||||
'allocation_alias' => 'sometimes|nullable|string|max:191',
|
||||
'allocation_ports' => 'required|array',
|
||||
];
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@ class AdvancedSettingsFormRequest extends AdminFormRequest
|
|||
{
|
||||
return [
|
||||
'recaptcha:enabled' => 'required|in:true,false',
|
||||
'recaptcha:secret_key' => 'required|string|max:255',
|
||||
'recaptcha:website_key' => 'required|string|max:255',
|
||||
'recaptcha:secret_key' => 'required|string|max:191',
|
||||
'recaptcha:website_key' => 'required|string|max:191',
|
||||
'pterodactyl:guzzle:timeout' => 'required|integer|between:1,60',
|
||||
'pterodactyl:guzzle:connect_timeout' => 'required|integer|between:1,60',
|
||||
'pterodactyl:console:count' => 'required|integer|min:1',
|
||||
|
|
|
@ -16,7 +16,7 @@ class BaseSettingsFormRequest extends AdminFormRequest
|
|||
public function rules()
|
||||
{
|
||||
return [
|
||||
'app:name' => 'required|string|max:255',
|
||||
'app:name' => 'required|string|max:191',
|
||||
'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2',
|
||||
'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))],
|
||||
'app:analytics' => 'nullable|string',
|
||||
|
|
|
@ -18,10 +18,10 @@ class MailSettingsFormRequest extends AdminFormRequest
|
|||
'mail:host' => 'required|string',
|
||||
'mail:port' => 'required|integer|between:1,65535',
|
||||
'mail:encryption' => ['present', Rule::in([null, 'tls', 'ssl'])],
|
||||
'mail:username' => 'nullable|string|max:255',
|
||||
'mail:password' => 'nullable|string|max:255',
|
||||
'mail:username' => 'nullable|string|max:191',
|
||||
'mail:password' => 'nullable|string|max:191',
|
||||
'mail:from:address' => 'required|string|email',
|
||||
'mail:from:name' => 'nullable|string|max:255',
|
||||
'mail:from:name' => 'nullable|string|max:191',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ class StoreAllocationRequest extends ApplicationApiRequest
|
|||
{
|
||||
return [
|
||||
'ip' => 'required|string',
|
||||
'alias' => 'sometimes|nullable|string|max:255',
|
||||
'alias' => 'sometimes|nullable|string|max:191',
|
||||
'ports' => 'required|array',
|
||||
'ports.*' => 'string',
|
||||
];
|
||||
|
|
|
@ -2,9 +2,12 @@
|
|||
|
||||
namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Pterodactyl\Services\Acl\Api\AdminAcl;
|
||||
use Pterodactyl\Services\Databases\DatabaseManagementService;
|
||||
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class StoreServerDatabaseRequest extends ApplicationApiRequest
|
||||
|
@ -26,14 +29,16 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest
|
|||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$server = $this->route()->parameter('server');
|
||||
|
||||
return [
|
||||
'database' => [
|
||||
'required',
|
||||
'string',
|
||||
'alpha_dash',
|
||||
'min:1',
|
||||
'max:24',
|
||||
Rule::unique('databases')->where(function (Builder $query) {
|
||||
$query->where('database_host_id', $this->input('host') ?? 0);
|
||||
'max:48',
|
||||
Rule::unique('databases')->where(function (Builder $query) use ($server) {
|
||||
$query->where('server_id', $server->id)->where('database', $this->databaseName());
|
||||
}),
|
||||
],
|
||||
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',
|
||||
|
@ -68,4 +73,18 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest
|
|||
'database' => 'Database Name',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the database name in the expected format.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function databaseName(): string
|
||||
{
|
||||
$server = $this->route()->parameter('server');
|
||||
|
||||
Assert::isInstanceOf($server, Server::class);
|
||||
|
||||
return DatabaseManagementService::generateUniqueDatabaseName($this->input('database'), $server->id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Account;
|
||||
|
||||
use Pterodactyl\Models\ApiKey;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
|
||||
class StoreApiKeyRequest extends ClientApiRequest
|
||||
|
@ -11,9 +12,11 @@ class StoreApiKeyRequest extends ClientApiRequest
|
|||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = ApiKey::getRules();
|
||||
|
||||
return [
|
||||
'description' => 'required|string|min:4',
|
||||
'allowed_ips' => 'array',
|
||||
'description' => $rules['memo'],
|
||||
'allowed_ips' => $rules['allowed_ips'],
|
||||
'allowed_ips.*' => 'ip',
|
||||
];
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ class StoreBackupRequest extends ClientApiRequest
|
|||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'nullable|string|max:255',
|
||||
'name' => 'nullable|string|max:191',
|
||||
'ignored' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
|
|
|
@ -2,9 +2,14 @@
|
|||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Databases;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
use Pterodactyl\Services\Databases\DatabaseManagementService;
|
||||
|
||||
class StoreDatabaseRequest extends ClientApiRequest implements ClientPermissionsRequest
|
||||
{
|
||||
|
@ -21,9 +26,35 @@ class StoreDatabaseRequest extends ClientApiRequest implements ClientPermissions
|
|||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$server = $this->route()->parameter('server');
|
||||
|
||||
Assert::isInstanceOf($server, Server::class);
|
||||
|
||||
return [
|
||||
'database' => 'required|alpha_dash|min:3|max:48',
|
||||
'database' => [
|
||||
'required',
|
||||
'alpha_dash',
|
||||
'min:1',
|
||||
'max:48',
|
||||
// Yes, I am aware that you could have the same database name across two unique hosts. However,
|
||||
// I don't really care about that for this validation. We just want to make sure it is unique to
|
||||
// the server itself. No need for complexity.
|
||||
Rule::unique('databases')->where(function (Builder $query) use ($server) {
|
||||
$query->where('server_id', $server->id)
|
||||
->where('database', DatabaseManagementService::generateUniqueDatabaseName($this->input('database'), $server->id));
|
||||
}),
|
||||
],
|
||||
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'database.unique' => 'The database name you have selected is already in use by this server.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ class UpdateStartupVariableRequest extends ClientApiRequest
|
|||
{
|
||||
return [
|
||||
'key' => 'required|string',
|
||||
'value' => 'present|string',
|
||||
'value' => 'present',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ class StoreSubuserRequest extends SubuserRequest
|
|||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => 'required|email',
|
||||
'email' => 'required|email|between:1,191',
|
||||
'permissions' => 'required|array',
|
||||
'permissions.*' => 'string',
|
||||
];
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
namespace Pterodactyl\Jobs\Schedule;
|
||||
|
||||
use Exception;
|
||||
use Carbon\Carbon;
|
||||
use Pterodactyl\Jobs\Job;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Pterodactyl\Models\Task;
|
||||
use InvalidArgumentException;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
@ -15,39 +15,25 @@ use Pterodactyl\Repositories\Eloquent\TaskRepository;
|
|||
use Pterodactyl\Services\Backups\InitiateBackupService;
|
||||
use Pterodactyl\Repositories\Wings\DaemonPowerRepository;
|
||||
use Pterodactyl\Repositories\Wings\DaemonCommandRepository;
|
||||
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
|
||||
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
|
||||
|
||||
class RunTaskJob extends Job implements ShouldQueue
|
||||
{
|
||||
use DispatchesJobs, InteractsWithQueue, SerializesModels;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $schedule;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
* @var \Pterodactyl\Models\Task
|
||||
*/
|
||||
public $task;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\TaskRepository
|
||||
*/
|
||||
protected $taskRepository;
|
||||
|
||||
/**
|
||||
* RunTaskJob constructor.
|
||||
*
|
||||
* @param int $task
|
||||
* @param int $schedule
|
||||
* @param \Pterodactyl\Models\Task $task
|
||||
*/
|
||||
public function __construct(int $task, int $schedule)
|
||||
public function __construct(Task $task)
|
||||
{
|
||||
$this->queue = config('pterodactyl.queues.standard');
|
||||
$this->task = $task;
|
||||
$this->schedule = $schedule;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,7 +44,6 @@ class RunTaskJob extends Job implements ShouldQueue
|
|||
* @param \Pterodactyl\Repositories\Wings\DaemonPowerRepository $powerRepository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\TaskRepository $taskRepository
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function handle(
|
||||
|
@ -67,36 +52,32 @@ class RunTaskJob extends Job implements ShouldQueue
|
|||
DaemonPowerRepository $powerRepository,
|
||||
TaskRepository $taskRepository
|
||||
) {
|
||||
$this->taskRepository = $taskRepository;
|
||||
|
||||
$task = $this->taskRepository->getTaskForJobProcess($this->task);
|
||||
$server = $task->getRelation('server');
|
||||
|
||||
// Do not process a task that is not set to active.
|
||||
if (! $task->getRelation('schedule')->is_active) {
|
||||
if (! $this->task->schedule->is_active) {
|
||||
$this->markTaskNotQueued();
|
||||
$this->markScheduleComplete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$server = $this->task->server;
|
||||
// Perform the provided task against the daemon.
|
||||
switch ($task->action) {
|
||||
switch ($this->task->action) {
|
||||
case 'power':
|
||||
$powerRepository->setServer($server)->send($task->payload);
|
||||
$powerRepository->setServer($server)->send($this->task->payload);
|
||||
break;
|
||||
case 'command':
|
||||
$commandRepository->setServer($server)->send($task->payload);
|
||||
$commandRepository->setServer($server)->send($this->task->payload);
|
||||
break;
|
||||
case 'backup':
|
||||
$backupService->setIgnoredFiles(explode(PHP_EOL, $task->payload))->handle($server, null);
|
||||
$backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException('Cannot run a task that points to a non-existent action.');
|
||||
}
|
||||
|
||||
$this->markTaskNotQueued();
|
||||
$this->queueNextTask($task->sequence_id);
|
||||
$this->queueNextTask();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -112,23 +93,23 @@ class RunTaskJob extends Job implements ShouldQueue
|
|||
|
||||
/**
|
||||
* Get the next task in the schedule and queue it for running after the defined period of wait time.
|
||||
*
|
||||
* @param int $sequence
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
private function queueNextTask($sequence)
|
||||
private function queueNextTask()
|
||||
{
|
||||
$nextTask = $this->taskRepository->getNextTask($this->schedule, $sequence);
|
||||
/** @var \Pterodactyl\Models\Task|null $nextTask */
|
||||
$nextTask = Task::query()->where('schedule_id', $this->task->schedule_id)
|
||||
->where('sequence_id', $this->task->sequence_id + 1)
|
||||
->first();
|
||||
|
||||
if (is_null($nextTask)) {
|
||||
$this->markScheduleComplete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->taskRepository->update($nextTask->id, ['is_queued' => true]);
|
||||
$this->dispatch((new self($nextTask->id, $this->schedule))->delay($nextTask->time_offset));
|
||||
$nextTask->update(['is_queued' => true]);
|
||||
|
||||
$this->dispatch((new self($nextTask))->delay($nextTask->time_offset));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -136,13 +117,10 @@ class RunTaskJob extends Job implements ShouldQueue
|
|||
*/
|
||||
private function markScheduleComplete()
|
||||
{
|
||||
Container::getInstance()
|
||||
->make(ScheduleRepositoryInterface::class)
|
||||
->withoutFreshModel()
|
||||
->update($this->schedule, [
|
||||
'is_processing' => false,
|
||||
'last_run_at' => Carbon::now()->toDateTimeString(),
|
||||
]);
|
||||
$this->task->schedule()->update([
|
||||
'is_processing' => false,
|
||||
'last_run_at' => CarbonImmutable::now()->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -150,8 +128,6 @@ class RunTaskJob extends Job implements ShouldQueue
|
|||
*/
|
||||
private function markTaskNotQueued()
|
||||
{
|
||||
Container::getInstance()
|
||||
->make(TaskRepositoryInterface::class)
|
||||
->update($this->task, ['is_queued' => false]);
|
||||
$this->task->update(['is_queued' => false]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ class DatabaseHost extends Model
|
|||
* @var array
|
||||
*/
|
||||
public static $validationRules = [
|
||||
'name' => 'required|string|max:255',
|
||||
'name' => 'required|string|max:191',
|
||||
'host' => 'required|string',
|
||||
'port' => 'required|numeric|between:1,65535',
|
||||
'username' => 'required|string|max:32',
|
||||
|
|
|
@ -93,13 +93,13 @@ class Egg extends Model
|
|||
public static $validationRules = [
|
||||
'nest_id' => 'required|bail|numeric|exists:nests,id',
|
||||
'uuid' => 'required|string|size:36',
|
||||
'name' => 'required|string|max:255',
|
||||
'name' => 'required|string|max:191',
|
||||
'description' => 'string|nullable',
|
||||
'author' => 'required|string|email',
|
||||
'docker_image' => 'required|string|max:255',
|
||||
'docker_image' => 'required|string|max:191',
|
||||
'startup' => 'required|nullable|string',
|
||||
'config_from' => 'sometimes|bail|nullable|numeric|exists:eggs,id',
|
||||
'config_stop' => 'required_without:config_from|nullable|string|max:255',
|
||||
'config_stop' => 'required_without:config_from|nullable|string|max:191',
|
||||
'config_startup' => 'required_without:config_from|nullable|json',
|
||||
'config_logs' => 'required_without:config_from|nullable|json',
|
||||
'config_files' => 'required_without:config_from|nullable|json',
|
||||
|
|
21
app/Models/EggMount.php
Normal file
21
app/Models/EggMount.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
class EggMount extends Model
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'egg_mount';
|
||||
|
||||
/**
|
||||
* @var null
|
||||
*/
|
||||
protected $primaryKey = null;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
}
|
|
@ -73,9 +73,9 @@ class EggVariable extends Model
|
|||
*/
|
||||
public static $validationRules = [
|
||||
'egg_id' => 'exists:eggs,id',
|
||||
'name' => 'required|string|between:1,255',
|
||||
'name' => 'required|string|between:1,191',
|
||||
'description' => 'string',
|
||||
'env_variable' => 'required|regex:/^[\w]{1,255}$/|notIn:' . self::RESERVED_ENV_NAMES,
|
||||
'env_variable' => 'required|regex:/^[\w]{1,191}$/|notIn:' . self::RESERVED_ENV_NAMES,
|
||||
'default_value' => 'string',
|
||||
'user_viewable' => 'boolean',
|
||||
'user_editable' => 'boolean',
|
||||
|
|
|
@ -41,7 +41,7 @@ class Location extends Model
|
|||
*/
|
||||
public static $validationRules = [
|
||||
'short' => 'required|string|between:1,60|unique:locations,short',
|
||||
'long' => 'string|nullable|between:1,255',
|
||||
'long' => 'string|nullable|between:1,191',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,7 @@ use Illuminate\Validation\Rule;
|
|||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Validation\Factory;
|
||||
use Illuminate\Database\Eloquent\Model as IlluminateModel;
|
||||
use Pterodactyl\Exceptions\Model\DataValidationException;
|
||||
|
||||
abstract class Model extends IlluminateModel
|
||||
{
|
||||
|
@ -55,7 +56,11 @@ abstract class Model extends IlluminateModel
|
|||
static::$validatorFactory = Container::getInstance()->make(Factory::class);
|
||||
|
||||
static::saving(function (Model $model) {
|
||||
return $model->validate();
|
||||
if (! $model->validate()) {
|
||||
throw new DataValidationException($model->getValidator());
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -147,9 +152,9 @@ abstract class Model extends IlluminateModel
|
|||
}
|
||||
|
||||
return $this->getValidator()->setData(
|
||||
// Trying to do self::toArray() here will leave out keys based on the whitelist/blacklist
|
||||
// for that model. Doing this will return all of the attributes in a format that can
|
||||
// properly be validated.
|
||||
// Trying to do self::toArray() here will leave out keys based on the whitelist/blacklist
|
||||
// for that model. Doing this will return all of the attributes in a format that can
|
||||
// properly be validated.
|
||||
$this->addCastAttributesToArray(
|
||||
$this->getAttributes(), $this->getMutatedAttributes()
|
||||
)
|
||||
|
|
|
@ -56,7 +56,7 @@ class Mount extends Model
|
|||
*/
|
||||
public static $validationRules = [
|
||||
'name' => 'required|string|min:2|max:64|unique:mounts,name',
|
||||
'description' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string|max:191',
|
||||
'source' => 'required|string',
|
||||
'target' => 'required|string',
|
||||
'read_only' => 'sometimes|boolean',
|
||||
|
|
|
@ -11,6 +11,11 @@ class MountServer extends Model
|
|||
*/
|
||||
protected $table = 'mount_server';
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* @var null
|
||||
*/
|
||||
|
|
|
@ -44,7 +44,7 @@ class Nest extends Model
|
|||
*/
|
||||
public static $validationRules = [
|
||||
'author' => 'required|string|email',
|
||||
'name' => 'required|string|max:255',
|
||||
'name' => 'required|string|max:191',
|
||||
'description' => 'nullable|string',
|
||||
];
|
||||
|
||||
|
|
|
@ -219,80 +219,4 @@ class Permission extends Model
|
|||
{
|
||||
return Collection::make(self::$permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of all permissions available for a user.
|
||||
*
|
||||
* @var array
|
||||
* @deprecated
|
||||
*/
|
||||
protected static $deprecatedPermissions = [
|
||||
'power' => [
|
||||
'power-start' => 's:power:start',
|
||||
'power-stop' => 's:power:stop',
|
||||
'power-restart' => 's:power:restart',
|
||||
'power-kill' => 's:power:kill',
|
||||
'send-command' => 's:command',
|
||||
],
|
||||
'subuser' => [
|
||||
'list-subusers' => null,
|
||||
'view-subuser' => null,
|
||||
'edit-subuser' => null,
|
||||
'create-subuser' => null,
|
||||
'delete-subuser' => null,
|
||||
],
|
||||
'server' => [
|
||||
'view-allocations' => null,
|
||||
'edit-allocation' => null,
|
||||
'view-startup' => null,
|
||||
'edit-startup' => null,
|
||||
],
|
||||
'database' => [
|
||||
'view-databases' => null,
|
||||
'reset-db-password' => null,
|
||||
'delete-database' => null,
|
||||
'create-database' => null,
|
||||
],
|
||||
'file' => [
|
||||
'access-sftp' => null,
|
||||
'list-files' => 's:files:get',
|
||||
'edit-files' => 's:files:read',
|
||||
'save-files' => 's:files:post',
|
||||
'move-files' => 's:files:move',
|
||||
'copy-files' => 's:files:copy',
|
||||
'compress-files' => 's:files:compress',
|
||||
'decompress-files' => 's:files:decompress',
|
||||
'create-files' => 's:files:create',
|
||||
'upload-files' => 's:files:upload',
|
||||
'delete-files' => 's:files:delete',
|
||||
'download-files' => 's:files:download',
|
||||
],
|
||||
'task' => [
|
||||
'list-schedules' => null,
|
||||
'view-schedule' => null,
|
||||
'toggle-schedule' => null,
|
||||
'queue-schedule' => null,
|
||||
'edit-schedule' => null,
|
||||
'create-schedule' => null,
|
||||
'delete-schedule' => null,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Return a collection of permissions available.
|
||||
*
|
||||
* @param bool $array
|
||||
* @return array|\Illuminate\Database\Eloquent\Collection
|
||||
* @deprecated
|
||||
*/
|
||||
public static function getPermissions($array = false)
|
||||
{
|
||||
if ($array) {
|
||||
return collect(self::$deprecatedPermissions)->mapWithKeys(function ($item) {
|
||||
return $item;
|
||||
})->all();
|
||||
}
|
||||
|
||||
return collect(self::$deprecatedPermissions);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ class Schedule extends Model
|
|||
*/
|
||||
public static $validationRules = [
|
||||
'server_id' => 'required|exists:servers,id',
|
||||
'name' => 'required|string|max:255',
|
||||
'name' => 'required|string|max:191',
|
||||
'cron_day_of_week' => 'required|string',
|
||||
'cron_day_of_month' => 'required|string',
|
||||
'cron_hour' => 'required|string',
|
||||
|
|
|
@ -15,7 +15,7 @@ use Znck\Eloquent\Traits\BelongsToThrough;
|
|||
* @property string $name
|
||||
* @property string $description
|
||||
* @property bool $skip_scripts
|
||||
* @property int $suspended
|
||||
* @property bool $suspended
|
||||
* @property int $owner_id
|
||||
* @property int $memory
|
||||
* @property int $swap
|
||||
|
@ -103,7 +103,7 @@ class Server extends Model
|
|||
public static $validationRules = [
|
||||
'external_id' => 'sometimes|nullable|string|between:1,191|unique:servers',
|
||||
'owner_id' => 'required|integer|exists:users,id',
|
||||
'name' => 'required|string|min:1|max:255',
|
||||
'name' => 'required|string|min:1|max:191',
|
||||
'node_id' => 'required|exists:nodes,id',
|
||||
'description' => 'string',
|
||||
'memory' => 'required|numeric|min:0',
|
||||
|
@ -118,7 +118,7 @@ class Server extends Model
|
|||
'egg_id' => 'required|exists:eggs,id',
|
||||
'startup' => 'required|string',
|
||||
'skip_scripts' => 'sometimes|boolean',
|
||||
'image' => 'required|string|max:255',
|
||||
'image' => 'required|string|max:191',
|
||||
'installed' => 'in:0,1,2',
|
||||
'database_limit' => 'present|nullable|integer|min:0',
|
||||
'allocation_limit' => 'sometimes|nullable|integer|min:0',
|
||||
|
@ -133,7 +133,7 @@ class Server extends Model
|
|||
protected $casts = [
|
||||
'node_id' => 'integer',
|
||||
'skip_scripts' => 'boolean',
|
||||
'suspended' => 'integer',
|
||||
'suspended' => 'boolean',
|
||||
'owner_id' => 'integer',
|
||||
'memory' => 'integer',
|
||||
'swap' => 'integer',
|
||||
|
|
|
@ -25,7 +25,7 @@ class Setting extends Model
|
|||
* @var array
|
||||
*/
|
||||
public static $validationRules = [
|
||||
'key' => 'required|string|between:1,255',
|
||||
'key' => 'required|string|between:1,191',
|
||||
'value' => 'string',
|
||||
];
|
||||
}
|
||||
|
|
|
@ -137,11 +137,11 @@ class User extends Model implements
|
|||
*/
|
||||
public static $validationRules = [
|
||||
'uuid' => 'required|string|size:36|unique:users,uuid',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'external_id' => 'sometimes|nullable|string|max:255|unique:users,external_id',
|
||||
'username' => 'required|between:1,255|unique:users,username',
|
||||
'name_first' => 'required|string|between:1,255',
|
||||
'name_last' => 'required|string|between:1,255',
|
||||
'email' => 'required|email|between:1,191|unique:users,email',
|
||||
'external_id' => 'sometimes|nullable|string|max:191|unique:users,external_id',
|
||||
'username' => 'required|between:1,191|unique:users,username',
|
||||
'name_first' => 'required|string|between:1,191',
|
||||
'name_last' => 'required|string|between:1,191',
|
||||
'password' => 'sometimes|nullable|string',
|
||||
'root_admin' => 'boolean',
|
||||
'language' => 'string',
|
||||
|
|
|
@ -93,31 +93,6 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor
|
|||
->paginate($count, $this->getColumns());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new database if it does not already exist on the host with
|
||||
* the provided details.
|
||||
*
|
||||
* @param array $data
|
||||
* @return \Pterodactyl\Models\Database
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException
|
||||
*/
|
||||
public function createIfNotExists(array $data): Database
|
||||
{
|
||||
$count = $this->getBuilder()->where([
|
||||
['server_id', '=', array_get($data, 'server_id')],
|
||||
['database_host_id', '=', array_get($data, 'database_host_id')],
|
||||
['database', '=', array_get($data, 'database')],
|
||||
])->count();
|
||||
|
||||
if ($count > 0) {
|
||||
throw new DuplicateDatabaseNameException('A database with those details already exists for the specified server.');
|
||||
}
|
||||
|
||||
return $this->create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new database on a given connection.
|
||||
*
|
||||
|
|
|
@ -171,28 +171,4 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
|
|||
|
||||
return $instance->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the IDs of all nodes that exist in the provided locations and have the space
|
||||
* available to support the additional disk and memory provided.
|
||||
*
|
||||
* @param array $locations
|
||||
* @param int $disk
|
||||
* @param int $memory
|
||||
* @return \Illuminate\Support\LazyCollection
|
||||
*/
|
||||
public function getNodesWithResourceUse(array $locations, int $disk, int $memory): LazyCollection
|
||||
{
|
||||
$instance = $this->getBuilder()
|
||||
->select(['nodes.id', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
|
||||
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
|
||||
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
|
||||
->where('nodes.public', 1);
|
||||
|
||||
if (! empty($locations)) {
|
||||
$instance->whereIn('nodes.location_id', $locations);
|
||||
}
|
||||
|
||||
return $instance->groupBy('nodes.id')->cursor();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,18 +3,29 @@
|
|||
namespace Pterodactyl\Services\Databases;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Database;
|
||||
use Pterodactyl\Helpers\Utilities;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Symfony\Component\VarDumper\Cloner\Data;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Pterodactyl\Extensions\DynamicDatabaseConnection;
|
||||
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
|
||||
use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
|
||||
use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException;
|
||||
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
|
||||
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
|
||||
|
||||
class DatabaseManagementService
|
||||
{
|
||||
/**
|
||||
* The regex used to validate that the database name passed through to the function is
|
||||
* in the expected format.
|
||||
*
|
||||
* @see \Pterodactyl\Services\Databases\DatabaseManagementService::generateUniqueDatabaseName()
|
||||
*/
|
||||
private const MATCH_NAME_REGEX = '/^(s[\d]+_)(.*)$/';
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Database\ConnectionInterface
|
||||
*/
|
||||
|
@ -31,7 +42,7 @@ class DatabaseManagementService
|
|||
private $encrypter;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
|
||||
* @var \Pterodactyl\Repositories\Eloquent\DatabaseRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
|
@ -50,13 +61,13 @@ class DatabaseManagementService
|
|||
*
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic
|
||||
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\DatabaseRepository $repository
|
||||
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
|
||||
*/
|
||||
public function __construct(
|
||||
ConnectionInterface $connection,
|
||||
DynamicDatabaseConnection $dynamic,
|
||||
DatabaseRepositoryInterface $repository,
|
||||
DatabaseRepository $repository,
|
||||
Encrypter $encrypter
|
||||
) {
|
||||
$this->connection = $connection;
|
||||
|
@ -65,6 +76,21 @@ class DatabaseManagementService
|
|||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique database name for the given server. This name should be passed through when
|
||||
* calling this handle function for this service, otherwise the database will be created with
|
||||
* whatever name is provided.
|
||||
*
|
||||
* @param string $name
|
||||
* @param int $serverId
|
||||
* @return string
|
||||
*/
|
||||
public static function generateUniqueDatabaseName(string $name, int $serverId): string
|
||||
{
|
||||
// Max of 48 characters, including the s123_ that we append to the front.
|
||||
return sprintf('s%d_%s', $serverId, substr($name, 0, 48 - strlen("s{$serverId}_")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set wether or not this class should validate that the server has enough slots
|
||||
* left before creating the new database.
|
||||
|
@ -104,12 +130,15 @@ class DatabaseManagementService
|
|||
}
|
||||
}
|
||||
|
||||
// Max of 48 characters, including the s123_ that we append to the front.
|
||||
$truncatedName = substr($data['database'], 0, 48 - strlen("s{$server->id}_"));
|
||||
// Protect against developer mistakes...
|
||||
if (empty($data['database']) || ! preg_match(self::MATCH_NAME_REGEX, $data['database'])) {
|
||||
throw new InvalidArgumentException(
|
||||
'The database name passed to DatabaseManagementService::handle MUST be prefixed with "s{server_id}_".'
|
||||
);
|
||||
}
|
||||
|
||||
$data = array_merge($data, [
|
||||
'server_id' => $server->id,
|
||||
'database' => $truncatedName,
|
||||
'username' => sprintf('u%d_%s', $server->id, str_random(10)),
|
||||
'password' => $this->encrypter->encrypt(
|
||||
Utilities::randomStringWithSpecialCharacters(24)
|
||||
|
@ -120,7 +149,8 @@ class DatabaseManagementService
|
|||
|
||||
try {
|
||||
return $this->connection->transaction(function () use ($data, &$database) {
|
||||
$database = $this->repository->createIfNotExists($data);
|
||||
$database = $this->createModel($data);
|
||||
|
||||
$this->dynamic->set('dynamic', $data['database_host_id']);
|
||||
|
||||
$this->repository->createDatabase($database->database);
|
||||
|
@ -139,7 +169,7 @@ class DatabaseManagementService
|
|||
$this->repository->dropUser($database->username, $database->remote);
|
||||
$this->repository->flush();
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
} catch (Exception $deletionException) {
|
||||
// Do nothing here. We've already encountered an issue before this point so no
|
||||
// reason to prioritize this error over the initial one.
|
||||
}
|
||||
|
@ -151,20 +181,48 @@ class DatabaseManagementService
|
|||
/**
|
||||
* Delete a database from the given host server.
|
||||
*
|
||||
* @param int $id
|
||||
* @param \Pterodactyl\Models\Database $database
|
||||
* @return bool|null
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function delete($id)
|
||||
public function delete(Database $database)
|
||||
{
|
||||
$database = $this->repository->find($id);
|
||||
$this->dynamic->set('dynamic', $database->database_host_id);
|
||||
|
||||
$this->repository->dropDatabase($database->database);
|
||||
$this->repository->dropUser($database->username, $database->remote);
|
||||
$this->repository->flush();
|
||||
|
||||
return $this->repository->delete($id);
|
||||
return $database->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the database if there is not an identical match in the DB. While you can technically
|
||||
* have the same name across multiple hosts, for the sake of keeping this logic easy to understand
|
||||
* and avoiding user confusion we will ignore the specific host and just look across all hosts.
|
||||
*
|
||||
* @param array $data
|
||||
* @return \Pterodactyl\Models\Database
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function createModel(array $data): Database
|
||||
{
|
||||
$exists = Database::query()->where('server_id', $data['server_id'])
|
||||
->where('database', $data['database'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
throw new DuplicateDatabaseNameException(
|
||||
'A database with that name already exists for this server.'
|
||||
);
|
||||
}
|
||||
|
||||
$database = (new Database)->forceFill($data);
|
||||
$database->saveOrFail();
|
||||
|
||||
return $database;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,44 +2,27 @@
|
|||
|
||||
namespace Pterodactyl\Services\Databases;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Database;
|
||||
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
|
||||
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
|
||||
use Pterodactyl\Models\DatabaseHost;
|
||||
use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException;
|
||||
|
||||
class DeployServerDatabaseService
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface
|
||||
*/
|
||||
private $databaseHostRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Databases\DatabaseManagementService
|
||||
*/
|
||||
private $managementService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* ServerDatabaseCreationService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
|
||||
* @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $databaseHostRepository
|
||||
* @param \Pterodactyl\Services\Databases\DatabaseManagementService $managementService
|
||||
*/
|
||||
public function __construct(
|
||||
DatabaseRepositoryInterface $repository,
|
||||
DatabaseHostRepositoryInterface $databaseHostRepository,
|
||||
DatabaseManagementService $managementService
|
||||
) {
|
||||
$this->databaseHostRepository = $databaseHostRepository;
|
||||
public function __construct(DatabaseManagementService $managementService)
|
||||
{
|
||||
$this->managementService = $managementService;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -53,28 +36,26 @@ class DeployServerDatabaseService
|
|||
*/
|
||||
public function handle(Server $server, array $data): Database
|
||||
{
|
||||
$allowRandom = config('pterodactyl.client_features.databases.allow_random');
|
||||
$hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([
|
||||
['node_id', '=', $server->node_id],
|
||||
]);
|
||||
|
||||
if ($hosts->isEmpty() && ! $allowRandom) {
|
||||
throw new NoSuitableDatabaseHostException;
|
||||
}
|
||||
Assert::notEmpty($data['database'] ?? null);
|
||||
Assert::notEmpty($data['remote'] ?? null);
|
||||
|
||||
$hosts = DatabaseHost::query()->get()->toBase();
|
||||
if ($hosts->isEmpty()) {
|
||||
$hosts = $this->databaseHostRepository->setColumns(['id'])->all();
|
||||
if ($hosts->isEmpty()) {
|
||||
throw new NoSuitableDatabaseHostException;
|
||||
} else {
|
||||
$nodeHosts = $hosts->where('node_id', $server->node_id)->toBase();
|
||||
|
||||
if ($nodeHosts->isEmpty() && ! config('pterodactyl.client_features.databases.allow_random')) {
|
||||
throw new NoSuitableDatabaseHostException;
|
||||
}
|
||||
}
|
||||
|
||||
$host = $hosts->random();
|
||||
|
||||
return $this->managementService->create($server, [
|
||||
'database_host_id' => $host->id,
|
||||
'database' => array_get($data, 'database'),
|
||||
'remote' => array_get($data, 'remote'),
|
||||
'database_host_id' => $nodeHosts->isEmpty()
|
||||
? $hosts->random()->id
|
||||
: $nodeHosts->random()->id,
|
||||
'database' => DatabaseManagementService::generateUniqueDatabaseName($data['database'], $server->id),
|
||||
'remote' => $data['remote'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,16 +3,12 @@
|
|||
namespace Pterodactyl\Services\Deployment;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
||||
use Pterodactyl\Models\Node;
|
||||
use Illuminate\Support\LazyCollection;
|
||||
use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException;
|
||||
|
||||
class FindViableNodesService
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
|
@ -28,16 +24,6 @@ class FindViableNodesService
|
|||
*/
|
||||
protected $memory;
|
||||
|
||||
/**
|
||||
* FindViableNodesService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
|
||||
*/
|
||||
public function __construct(NodeRepositoryInterface $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the locations that should be searched through to locate available nodes.
|
||||
*
|
||||
|
@ -46,6 +32,8 @@ class FindViableNodesService
|
|||
*/
|
||||
public function setLocations(array $locations): self
|
||||
{
|
||||
Assert::allInteger($locations, 'An array of location IDs should be provided when calling setLocations.');
|
||||
|
||||
$this->locations = $locations;
|
||||
|
||||
return $this;
|
||||
|
@ -90,32 +78,34 @@ class FindViableNodesService
|
|||
* are tossed out, as are any nodes marked as non-public, meaning automatic
|
||||
* deployments should not be done against them.
|
||||
*
|
||||
* @return int[]
|
||||
* @return \Pterodactyl\Models\Node[]|\Illuminate\Support\Collection
|
||||
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException
|
||||
*/
|
||||
public function handle(): array
|
||||
public function handle()
|
||||
{
|
||||
Assert::integer($this->disk, 'Calls to ' . __METHOD__ . ' must have the disk space set as an integer, received %s');
|
||||
Assert::integer($this->memory, 'Calls to ' . __METHOD__ . ' must have the memory usage set as an integer, received %s');
|
||||
Assert::integer($this->disk, 'Disk space must be an int, got %s');
|
||||
Assert::integer($this->memory, 'Memory usage must be an int, got %s');
|
||||
|
||||
$nodes = $this->repository->getNodesWithResourceUse($this->locations, $this->disk, $this->memory);
|
||||
$viable = [];
|
||||
$query = Node::query()->select('nodes.*')
|
||||
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory')
|
||||
->selectRaw('IFNULL(SUM(servers.disk), 0) as sum_disk')
|
||||
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
|
||||
->where('nodes.public', 1);
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
$memoryLimit = $node->memory * (1 + ($node->memory_overallocate / 100));
|
||||
$diskLimit = $node->disk * (1 + ($node->disk_overallocate / 100));
|
||||
|
||||
if (($node->sum_memory + $this->memory) > $memoryLimit || ($node->sum_disk + $this->disk) > $diskLimit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$viable[] = $node->id;
|
||||
if (! empty($this->locations)) {
|
||||
$query = $query->whereIn('nodes.location_id', $this->locations);
|
||||
}
|
||||
|
||||
if (empty($viable)) {
|
||||
$results = $query->groupBy('nodes.id')
|
||||
->havingRaw('(IFNULL(SUM(servers.memory), 0) + ?) <= (nodes.memory * (1 + (nodes.memory_overallocate / 100)))', [ $this->memory ])
|
||||
->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [ $this->disk ])
|
||||
->get()
|
||||
->toBase();
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
throw new NoViableNodeException(trans('exceptions.deployment.no_viable_nodes'));
|
||||
}
|
||||
|
||||
return $viable;
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,4 @@
|
|||
<?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\Eggs\Scripts;
|
||||
|
||||
|
@ -40,12 +33,8 @@ class InstallScriptService
|
|||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @throws \Pterodactyl\Exceptions\Service\Egg\InvalidCopyFromException
|
||||
*/
|
||||
public function handle($egg, array $data)
|
||||
public function handle(Egg $egg, array $data)
|
||||
{
|
||||
if (! $egg instanceof Egg) {
|
||||
$egg = $this->repository->find($egg);
|
||||
}
|
||||
|
||||
if (! is_null(array_get($data, 'copy_script_from'))) {
|
||||
if (! $this->repository->isCopyableScript(array_get($data, 'copy_script_from'), $egg->nest_id)) {
|
||||
throw new InvalidCopyFromException(trans('exceptions.nest.egg.invalid_copy_id'));
|
||||
|
|
|
@ -1,11 +1,4 @@
|
|||
<?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\Eggs\Sharing;
|
||||
|
||||
|
|
|
@ -1,11 +1,4 @@
|
|||
<?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\Eggs\Sharing;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Pterodactyl\Services\Eggs\Sharing;
|
||||
|
||||
use Pterodactyl\Models\Egg;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
|
||||
|
@ -46,7 +47,7 @@ class EggUpdateImporterService
|
|||
/**
|
||||
* Update an existing Egg using an uploaded JSON file.
|
||||
*
|
||||
* @param int $egg
|
||||
* @param \Pterodactyl\Models\Egg $egg
|
||||
* @param \Illuminate\Http\UploadedFile $file
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
|
@ -54,7 +55,7 @@ class EggUpdateImporterService
|
|||
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
|
||||
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
|
||||
*/
|
||||
public function handle(int $egg, UploadedFile $file)
|
||||
public function handle(Egg $egg, UploadedFile $file)
|
||||
{
|
||||
if ($file->getError() !== UPLOAD_ERR_OK || ! $file->isFile()) {
|
||||
throw new InvalidFileUploadException(
|
||||
|
@ -81,7 +82,7 @@ class EggUpdateImporterService
|
|||
}
|
||||
|
||||
$this->connection->beginTransaction();
|
||||
$this->repository->update($egg, [
|
||||
$this->repository->update($egg->id, [
|
||||
'author' => object_get($parsed, 'author'),
|
||||
'name' => object_get($parsed, 'name'),
|
||||
'description' => object_get($parsed, 'description'),
|
||||
|
@ -99,19 +100,19 @@ class EggUpdateImporterService
|
|||
// Update Existing Variables
|
||||
collect($parsed->variables)->each(function ($variable) use ($egg) {
|
||||
$this->variableRepository->withoutFreshModel()->updateOrCreate([
|
||||
'egg_id' => $egg,
|
||||
'egg_id' => $egg->id,
|
||||
'env_variable' => $variable->env_variable,
|
||||
], collect($variable)->except(['egg_id', 'env_variable'])->toArray());
|
||||
});
|
||||
|
||||
$imported = collect($parsed->variables)->pluck('env_variable')->toArray();
|
||||
$existing = $this->variableRepository->setColumns(['id', 'env_variable'])->findWhere([['egg_id', '=', $egg]]);
|
||||
$existing = $this->variableRepository->setColumns(['id', 'env_variable'])->findWhere([['egg_id', '=', $egg->id]]);
|
||||
|
||||
// Delete variables not present in the import.
|
||||
collect($existing)->each(function ($variable) use ($egg, $imported) {
|
||||
if (! in_array($variable->env_variable, $imported)) {
|
||||
$this->variableRepository->deleteWhere([
|
||||
['egg_id', '=', $egg],
|
||||
['egg_id', '=', $egg->id],
|
||||
['env_variable', '=', $variable->env_variable],
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Services\Mounts;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Pterodactyl\Repositories\Eloquent\MountRepository;
|
||||
|
||||
class MountCreationService
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\MountRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* MountCreationService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Eloquent\MountRepository $repository
|
||||
*/
|
||||
public function __construct(MountRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new mount.
|
||||
*
|
||||
* @param array $data
|
||||
* @return \Pterodactyl\Models\Mount
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function handle(array $data)
|
||||
{
|
||||
return $this->repository->create(array_merge($data, [
|
||||
'uuid' => Uuid::uuid4()->toString(),
|
||||
]), true, true);
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Services\Mounts;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Mount;
|
||||
use Pterodactyl\Repositories\Eloquent\MountRepository;
|
||||
|
||||
class MountDeletionService
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\MountRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* MountDeletionService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Eloquent\MountRepository $repository
|
||||
*/
|
||||
public function __construct(MountRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing location.
|
||||
*
|
||||
* @param int|\Pterodactyl\Models\Mount $mount
|
||||
* @return int|null
|
||||
*/
|
||||
public function handle($mount)
|
||||
{
|
||||
$mount = ($mount instanceof Mount) ? $mount->id : $mount;
|
||||
|
||||
Assert::integerish($mount, 'First argument passed to handle must be numeric or an instance of ' . Mount::class . ', received %s.');
|
||||
|
||||
return $this->repository->delete($mount);
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Services\Mounts;
|
||||
|
||||
use Pterodactyl\Models\Mount;
|
||||
use Pterodactyl\Repositories\Eloquent\MountRepository;
|
||||
|
||||
class MountUpdateService
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\MountRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* MountUpdateService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Eloquent\MountRepository $repository
|
||||
*/
|
||||
public function __construct(MountRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing location.
|
||||
*
|
||||
* @param int|\Pterodactyl\Models\Mount $mount
|
||||
* @param array $data
|
||||
* @return \Pterodactyl\Models\Mount
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function handle($mount, array $data)
|
||||
{
|
||||
$mount = ($mount instanceof Mount) ? $mount->id : $mount;
|
||||
|
||||
return $this->repository->update($mount, $data);
|
||||
}
|
||||
}
|
|
@ -73,7 +73,7 @@ class ProcessScheduleService
|
|||
$this->taskRepository->update($task->id, ['is_queued' => true]);
|
||||
|
||||
$this->dispatcher->dispatch(
|
||||
(new RunTaskJob($task->id, $schedule->id))->delay($task->time_offset)
|
||||
(new RunTaskJob($task))->delay($task->time_offset)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,6 @@ namespace Pterodactyl\Services\Servers;
|
|||
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\EggVariable;
|
||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
|
||||
class EnvironmentService
|
||||
{
|
||||
|
@ -14,28 +12,6 @@ class EnvironmentService
|
|||
*/
|
||||
private $additional = [];
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Config\Repository
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* EnvironmentService constructor.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Config\Repository $config
|
||||
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
|
||||
*/
|
||||
public function __construct(ConfigRepository $config, ServerRepositoryInterface $repository)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically configure additional environment variables to be assigned
|
||||
* with a specific server.
|
||||
|
@ -79,7 +55,7 @@ class EnvironmentService
|
|||
}
|
||||
|
||||
// Process variables set in the configuration file.
|
||||
foreach ($this->config->get('pterodactyl.environment_variables', []) as $key => $object) {
|
||||
foreach (config('pterodactyl.environment_variables', []) as $key => $object) {
|
||||
$variables->put(
|
||||
$key, is_callable($object) ? call_user_func($object, $server) : object_get($server, $object)
|
||||
);
|
||||
|
|
|
@ -19,26 +19,18 @@ class ReinstallServerService
|
|||
*/
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* ReinstallService constructor.
|
||||
*
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
|
||||
*/
|
||||
public function __construct(
|
||||
ConnectionInterface $connection,
|
||||
DaemonServerRepository $daemonServerRepository,
|
||||
ServerRepository $repository
|
||||
DaemonServerRepository $daemonServerRepository
|
||||
) {
|
||||
$this->daemonServerRepository = $daemonServerRepository;
|
||||
$this->connection = $connection;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,16 +41,14 @@ class ReinstallServerService
|
|||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function reinstall(Server $server)
|
||||
public function handle(Server $server)
|
||||
{
|
||||
return $this->connection->transaction(function () use ($server) {
|
||||
$updated = $this->repository->update($server->id, [
|
||||
'installed' => Server::STATUS_INSTALLING,
|
||||
], true, true);
|
||||
$server->forceFill(['installed' => Server::STATUS_INSTALLING])->save();
|
||||
|
||||
$this->daemonServerRepository->setServer($server)->reinstall();
|
||||
|
||||
return $updated;
|
||||
return $server->refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,9 @@
|
|||
<?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\Servers;
|
||||
|
||||
use Pterodactyl\Models\Mount;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
|
||||
class ServerConfigurationStructureService
|
||||
{
|
||||
|
@ -22,22 +14,13 @@ class ServerConfigurationStructureService
|
|||
*/
|
||||
private $environment;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* ServerConfigurationStructureService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
|
||||
* @param \Pterodactyl\Services\Servers\EnvironmentService $environment
|
||||
*/
|
||||
public function __construct(
|
||||
ServerRepositoryInterface $repository,
|
||||
EnvironmentService $environment
|
||||
) {
|
||||
$this->repository = $repository;
|
||||
public function __construct(EnvironmentService $environment)
|
||||
{
|
||||
$this->environment = $environment;
|
||||
}
|
||||
|
||||
|
@ -50,8 +33,6 @@ class ServerConfigurationStructureService
|
|||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param bool $legacy
|
||||
* @return array
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function handle(Server $server, bool $legacy = false): array
|
||||
{
|
||||
|
@ -72,7 +53,7 @@ class ServerConfigurationStructureService
|
|||
{
|
||||
return [
|
||||
'uuid' => $server->uuid,
|
||||
'suspended' => (bool) $server->suspended,
|
||||
'suspended' => $server->suspended,
|
||||
'environment' => $this->environment->handle($server),
|
||||
'invocation' => $server->startup,
|
||||
'skip_egg_scripts' => $server->skip_scripts,
|
||||
|
@ -112,8 +93,6 @@ class ServerConfigurationStructureService
|
|||
*
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return array
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
protected function returnLegacyFormat(Server $server)
|
||||
{
|
||||
|
|
|
@ -4,7 +4,9 @@ namespace Pterodactyl\Services\Servers;
|
|||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Illuminate\Support\Arr;
|
||||
use Pterodactyl\Models\Egg;
|
||||
use Pterodactyl\Models\User;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Support\Collection;
|
||||
use Pterodactyl\Models\Allocation;
|
||||
|
@ -13,7 +15,6 @@ use Pterodactyl\Models\Objects\DeploymentObject;
|
|||
use Pterodactyl\Repositories\Eloquent\EggRepository;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
|
||||
use Pterodactyl\Repositories\Eloquent\AllocationRepository;
|
||||
use Pterodactyl\Services\Deployment\FindViableNodesService;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerVariableRepository;
|
||||
use Pterodactyl\Services\Deployment\AllocationSelectionService;
|
||||
|
@ -21,11 +22,6 @@ use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
|||
|
||||
class ServerCreationService
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\AllocationRepository
|
||||
*/
|
||||
private $allocationRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Deployment\AllocationSelectionService
|
||||
*/
|
||||
|
@ -79,7 +75,6 @@ class ServerCreationService
|
|||
/**
|
||||
* CreationService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Eloquent\AllocationRepository $allocationRepository
|
||||
* @param \Pterodactyl\Services\Deployment\AllocationSelectionService $allocationSelectionService
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
|
||||
|
@ -92,7 +87,6 @@ class ServerCreationService
|
|||
* @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService
|
||||
*/
|
||||
public function __construct(
|
||||
AllocationRepository $allocationRepository,
|
||||
AllocationSelectionService $allocationSelectionService,
|
||||
ConnectionInterface $connection,
|
||||
DaemonServerRepository $daemonServerRepository,
|
||||
|
@ -105,7 +99,6 @@ class ServerCreationService
|
|||
VariableValidatorService $validatorService
|
||||
) {
|
||||
$this->allocationSelectionService = $allocationSelectionService;
|
||||
$this->allocationRepository = $allocationRepository;
|
||||
$this->configurationStructureService = $configurationStructureService;
|
||||
$this->connection = $connection;
|
||||
$this->findViableNodesService = $findViableNodesService;
|
||||
|
@ -130,15 +123,12 @@ class ServerCreationService
|
|||
* @throws \Throwable
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException
|
||||
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException
|
||||
*/
|
||||
public function handle(array $data, DeploymentObject $deployment = null): Server
|
||||
{
|
||||
$this->connection->beginTransaction();
|
||||
|
||||
// If a deployment object has been passed we need to get the allocation
|
||||
// that the server should use, and assign the node from that allocation.
|
||||
if ($deployment instanceof DeploymentObject) {
|
||||
|
@ -149,37 +139,42 @@ class ServerCreationService
|
|||
|
||||
// Auto-configure the node based on the selected allocation
|
||||
// if no node was defined.
|
||||
if (is_null(Arr::get($data, 'node_id'))) {
|
||||
$data['node_id'] = $this->getNodeFromAllocation($data['allocation_id']);
|
||||
if (empty($data['node_id'])) {
|
||||
Assert::false(empty($data['allocation_id']), 'Expected a non-empty allocation_id in server creation data.');
|
||||
|
||||
$data['node_id'] = Allocation::query()->findOrFail($data['allocation_id'])->node_id;
|
||||
}
|
||||
|
||||
if (is_null(Arr::get($data, 'nest_id'))) {
|
||||
/** @var \Pterodactyl\Models\Egg $egg */
|
||||
$egg = $this->eggRepository->setColumns(['id', 'nest_id'])->find(Arr::get($data, 'egg_id'));
|
||||
$data['nest_id'] = $egg->nest_id;
|
||||
if (empty($data['nest_id'])) {
|
||||
Assert::false(empty($data['egg_id']), 'Expected a non-empty egg_id in server creation data.');
|
||||
|
||||
$data['nest_id'] = Egg::query()->findOrFail($data['egg_id'])->nest_id;
|
||||
}
|
||||
|
||||
$eggVariableData = $this->validatorService
|
||||
->setUserLevel(User::USER_LEVEL_ADMIN)
|
||||
->handle(Arr::get($data, 'egg_id'), Arr::get($data, 'environment', []));
|
||||
|
||||
// Create the server and assign any additional allocations to it.
|
||||
$server = $this->createModel($data);
|
||||
|
||||
$this->storeAssignedAllocations($server, $data);
|
||||
$this->storeEggVariables($server, $eggVariableData);
|
||||
|
||||
// Due to the design of the Daemon, we need to persist this server to the disk
|
||||
// before we can actually create it on the Daemon.
|
||||
//
|
||||
// If that connection fails out we will attempt to perform a cleanup by just
|
||||
// deleting the server itself from the system.
|
||||
$this->connection->commit();
|
||||
/** @var \Pterodactyl\Models\Server $server */
|
||||
$server = $this->connection->transaction(function () use ($data, $eggVariableData) {
|
||||
// Create the server and assign any additional allocations to it.
|
||||
$server = $this->createModel($data);
|
||||
|
||||
$structure = $this->configurationStructureService->handle($server);
|
||||
$this->storeAssignedAllocations($server, $data);
|
||||
$this->storeEggVariables($server, $eggVariableData);
|
||||
|
||||
return $server;
|
||||
});
|
||||
|
||||
try {
|
||||
$this->daemonServerRepository->setServer($server)->create($structure);
|
||||
$this->daemonServerRepository->setServer($server)->create(
|
||||
$this->configurationStructureService->handle($server)
|
||||
);
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
$this->serverDeletionService->withForce(true)->handle($server);
|
||||
|
||||
|
@ -208,7 +203,7 @@ class ServerCreationService
|
|||
->handle();
|
||||
|
||||
return $this->allocationSelectionService->setDedicated($deployment->isDedicated())
|
||||
->setNodes($nodes)
|
||||
->setNodes($nodes->pluck('id')->toArray())
|
||||
->setPorts($deployment->getPorts())
|
||||
->handle();
|
||||
}
|
||||
|
@ -269,7 +264,7 @@ class ServerCreationService
|
|||
$records = array_merge($records, $data['allocation_additional']);
|
||||
}
|
||||
|
||||
$this->allocationRepository->updateWhereIn('id', $records, [
|
||||
Allocation::query()->whereIn('id', $records)->update([
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
}
|
||||
|
@ -295,22 +290,6 @@ class ServerCreationService
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node that an allocation belongs to.
|
||||
*
|
||||
* @param int $id
|
||||
* @return int
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
private function getNodeFromAllocation(int $id): int
|
||||
{
|
||||
/** @var \Pterodactyl\Models\Allocation $allocation */
|
||||
$allocation = $this->allocationRepository->setColumns(['id', 'node_id'])->find($id);
|
||||
|
||||
return $allocation->node_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique UUID and UUID-Short combo for a server.
|
||||
*
|
||||
|
|
|
@ -3,11 +3,10 @@
|
|||
namespace Pterodactyl\Services\Servers;
|
||||
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
|
||||
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
|
||||
use Pterodactyl\Services\Databases\DatabaseManagementService;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
@ -29,50 +28,26 @@ class ServerDeletionService
|
|||
*/
|
||||
private $daemonServerRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\DatabaseRepository
|
||||
*/
|
||||
private $databaseRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Databases\DatabaseManagementService
|
||||
*/
|
||||
private $databaseManagementService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Psr\Log\LoggerInterface
|
||||
*/
|
||||
private $writer;
|
||||
|
||||
/**
|
||||
* DeletionService constructor.
|
||||
*
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\DatabaseRepository $databaseRepository
|
||||
* @param \Pterodactyl\Services\Databases\DatabaseManagementService $databaseManagementService
|
||||
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
|
||||
* @param \Psr\Log\LoggerInterface $writer
|
||||
*/
|
||||
public function __construct(
|
||||
ConnectionInterface $connection,
|
||||
DaemonServerRepository $daemonServerRepository,
|
||||
DatabaseRepository $databaseRepository,
|
||||
DatabaseManagementService $databaseManagementService,
|
||||
ServerRepository $repository,
|
||||
LoggerInterface $writer
|
||||
DatabaseManagementService $databaseManagementService
|
||||
) {
|
||||
$this->connection = $connection;
|
||||
$this->daemonServerRepository = $daemonServerRepository;
|
||||
$this->databaseRepository = $databaseRepository;
|
||||
$this->databaseManagementService = $databaseManagementService;
|
||||
$this->repository = $repository;
|
||||
$this->writer = $writer;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -101,27 +76,39 @@ class ServerDeletionService
|
|||
try {
|
||||
$this->daemonServerRepository->setServer($server)->delete();
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
if ($this->force) {
|
||||
$this->writer->warning($exception);
|
||||
} else {
|
||||
// If there is an error not caused a 404 error and this isn't a forced delete,
|
||||
// go ahead and bail out. We specifically ignore a 404 since that can be assumed
|
||||
// to be a safe error, meaning the server doesn't exist at all on Wings so there
|
||||
// is no reason we need to bail out from that.
|
||||
if (! $this->force && $exception->getStatusCode() !== Response::HTTP_NOT_FOUND) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
Log::warning($exception);
|
||||
}
|
||||
|
||||
$this->connection->transaction(function () use ($server) {
|
||||
$this->databaseRepository->setColumns('id')->findWhere([['server_id', '=', $server->id]])->each(function ($item) {
|
||||
foreach ($server->databases as $database) {
|
||||
try {
|
||||
$this->databaseManagementService->delete($item->id);
|
||||
$this->databaseManagementService->delete($database);
|
||||
} catch (Exception $exception) {
|
||||
if ($this->force) {
|
||||
$this->writer->warning($exception);
|
||||
} else {
|
||||
if (!$this->force) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->repository->delete($server->id);
|
||||
// Oh well, just try to delete the database entry we have from the database
|
||||
// so that the server itself can be deleted. This will leave it dangling on
|
||||
// the host instance, but we couldn't delete it anyways so not sure how we would
|
||||
// handle this better anyways.
|
||||
//
|
||||
// @see https://github.com/pterodactyl/panel/issues/2085
|
||||
$database->delete();
|
||||
|
||||
Log::warning($exception);
|
||||
}
|
||||
}
|
||||
|
||||
$server->delete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
namespace Pterodactyl\Services\Servers;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Pterodactyl\Models\Egg;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\ServerVariable;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Traits\Services\HasUserLevels;
|
||||
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface;
|
||||
|
||||
class StartupModificationService
|
||||
{
|
||||
|
@ -19,63 +19,21 @@ class StartupModificationService
|
|||
*/
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface
|
||||
*/
|
||||
private $eggRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Servers\EnvironmentService
|
||||
*/
|
||||
private $environmentService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface
|
||||
*/
|
||||
private $serverVariableRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Servers\VariableValidatorService
|
||||
*/
|
||||
private $validatorService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService
|
||||
*/
|
||||
private $structureService;
|
||||
|
||||
/**
|
||||
* StartupModificationService constructor.
|
||||
*
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Pterodactyl\Contracts\Repository\EggRepositoryInterface $eggRepository
|
||||
* @param \Pterodactyl\Services\Servers\EnvironmentService $environmentService
|
||||
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
|
||||
* @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $structureService
|
||||
* @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository
|
||||
* @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService
|
||||
*/
|
||||
public function __construct(
|
||||
ConnectionInterface $connection,
|
||||
EggRepositoryInterface $eggRepository,
|
||||
EnvironmentService $environmentService,
|
||||
ServerRepositoryInterface $repository,
|
||||
ServerConfigurationStructureService $structureService,
|
||||
ServerVariableRepositoryInterface $serverVariableRepository,
|
||||
VariableValidatorService $validatorService
|
||||
) {
|
||||
public function __construct(ConnectionInterface $connection, VariableValidatorService $validatorService)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->eggRepository = $eggRepository;
|
||||
$this->environmentService = $environmentService;
|
||||
$this->repository = $repository;
|
||||
$this->serverVariableRepository = $serverVariableRepository;
|
||||
$this->validatorService = $validatorService;
|
||||
$this->structureService = $structureService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -85,34 +43,42 @@ class StartupModificationService
|
|||
* @param array $data
|
||||
* @return \Pterodactyl\Models\Server
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function handle(Server $server, array $data): Server
|
||||
{
|
||||
$this->connection->beginTransaction();
|
||||
if (! is_null(array_get($data, 'environment'))) {
|
||||
$this->validatorService->setUserLevel($this->getUserLevel());
|
||||
$results = $this->validatorService->handle(array_get($data, 'egg_id', $server->egg_id), array_get($data, 'environment', []));
|
||||
return $this->connection->transaction(function () use ($server, $data) {
|
||||
if (! empty($data['environment'])) {
|
||||
$egg = $this->isUserLevel(User::USER_LEVEL_ADMIN) ? ($data['egg_id'] ?? $server->egg_id) : $server->egg_id;
|
||||
|
||||
$results->each(function ($result) use ($server) {
|
||||
$this->serverVariableRepository->withoutFreshModel()->updateOrCreate([
|
||||
'server_id' => $server->id,
|
||||
'variable_id' => $result->id,
|
||||
], [
|
||||
'variable_value' => $result->value ?? '',
|
||||
]);
|
||||
});
|
||||
}
|
||||
$results = $this->validatorService
|
||||
->setUserLevel($this->getUserLevel())
|
||||
->handle($egg, $data['environment']);
|
||||
|
||||
if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) {
|
||||
$this->updateAdministrativeSettings($data, $server);
|
||||
}
|
||||
foreach ($results as $result) {
|
||||
ServerVariable::query()->updateOrCreate(
|
||||
[
|
||||
'server_id' => $server->id,
|
||||
'variable_id' => $result->id,
|
||||
],
|
||||
['variable_value' => $result->value ?? '']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->connection->commit();
|
||||
if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) {
|
||||
$this->updateAdministrativeSettings($data, $server);
|
||||
}
|
||||
|
||||
return $server;
|
||||
// Calling ->refresh() rather than ->fresh() here causes it to return the
|
||||
// variables as triplicates for some reason? Not entirely sure, should dig
|
||||
// in more to figure it out, but luckily we have a test case covering this
|
||||
// specific call so we can be assured we're not breaking it _here_ at least.
|
||||
//
|
||||
// TODO(dane): this seems like a red-flag for the code powering the relationship
|
||||
// that should be looked into more.
|
||||
return $server->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -120,28 +86,25 @@ class StartupModificationService
|
|||
*
|
||||
* @param array $data
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
private function updateAdministrativeSettings(array $data, Server &$server)
|
||||
protected function updateAdministrativeSettings(array $data, Server &$server)
|
||||
{
|
||||
if (
|
||||
is_digit(array_get($data, 'egg_id'))
|
||||
&& $data['egg_id'] != $server->egg_id
|
||||
&& is_null(array_get($data, 'nest_id'))
|
||||
) {
|
||||
$egg = $this->eggRepository->setColumns(['id', 'nest_id'])->find($data['egg_id']);
|
||||
$data['nest_id'] = $egg->nest_id;
|
||||
$eggId = Arr::get($data, 'egg_id');
|
||||
|
||||
if (is_digit($eggId) && $server->egg_id !== (int)$eggId) {
|
||||
/** @var \Pterodactyl\Models\Egg $egg */
|
||||
$egg = Egg::query()->findOrFail($data['egg_id']);
|
||||
|
||||
$server = $server->forceFill([
|
||||
'egg_id' => $egg->id,
|
||||
'nest_id' => $egg->nest_id,
|
||||
]);
|
||||
}
|
||||
|
||||
$server = $this->repository->update($server->id, [
|
||||
'installed' => 0,
|
||||
'startup' => array_get($data, 'startup', $server->startup),
|
||||
'nest_id' => array_get($data, 'nest_id', $server->nest_id),
|
||||
'egg_id' => array_get($data, 'egg_id', $server->egg_id),
|
||||
'skip_scripts' => array_get($data, 'skip_scripts') ?? isset($data['skip_scripts']),
|
||||
'image' => array_get($data, 'docker_image', $server->image),
|
||||
]);
|
||||
$server->fill([
|
||||
'startup' => $data['startup'] ?? $server->startup,
|
||||
'skip_scripts' => $data['skip_scripts'] ?? isset($data['skip_scripts']),
|
||||
'image' => $data['docker_image'] ?? $server->image,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,10 @@
|
|||
|
||||
namespace Pterodactyl\Services\Servers;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
|
||||
class SuspensionService
|
||||
{
|
||||
|
@ -19,16 +17,6 @@ class SuspensionService
|
|||
*/
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Psr\Log\LoggerInterface
|
||||
*/
|
||||
private $writer;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Wings\DaemonServerRepository
|
||||
*/
|
||||
|
@ -39,25 +27,19 @@ class SuspensionService
|
|||
*
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
|
||||
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
|
||||
* @param \Psr\Log\LoggerInterface $writer
|
||||
*/
|
||||
public function __construct(
|
||||
ConnectionInterface $connection,
|
||||
DaemonServerRepository $daemonServerRepository,
|
||||
ServerRepositoryInterface $repository,
|
||||
LoggerInterface $writer
|
||||
DaemonServerRepository $daemonServerRepository
|
||||
) {
|
||||
$this->connection = $connection;
|
||||
$this->repository = $repository;
|
||||
$this->writer = $writer;
|
||||
$this->daemonServerRepository = $daemonServerRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspends a server on the system.
|
||||
*
|
||||
* @param int|\Pterodactyl\Models\Server $server
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param string $action
|
||||
*
|
||||
* @throws \Throwable
|
||||
|
@ -66,15 +48,16 @@ class SuspensionService
|
|||
{
|
||||
Assert::oneOf($action, [self::ACTION_SUSPEND, self::ACTION_UNSUSPEND]);
|
||||
|
||||
if (
|
||||
$action === self::ACTION_SUSPEND && $server->suspended ||
|
||||
$action === self::ACTION_UNSUSPEND && ! $server->suspended
|
||||
) {
|
||||
$isSuspending = $action === self::ACTION_SUSPEND;
|
||||
// Nothing needs to happen if we're suspending the server and it is already
|
||||
// suspended in the database. Additionally, nothing needs to happen if the server
|
||||
// is not suspended and we try to un-suspend the instance.
|
||||
if ($isSuspending === $server->suspended) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->connection->transaction(function () use ($action, $server) {
|
||||
$this->repository->withoutFreshModel()->update($server->id, [
|
||||
$server->update([
|
||||
'suspended' => $action === self::ACTION_SUSPEND,
|
||||
]);
|
||||
|
||||
|
|
|
@ -11,32 +11,15 @@ namespace Pterodactyl\Services\Servers;
|
|||
|
||||
use Pterodactyl\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Pterodactyl\Models\EggVariable;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Pterodactyl\Traits\Services\HasUserLevels;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
|
||||
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
|
||||
use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface;
|
||||
|
||||
class VariableValidatorService
|
||||
{
|
||||
use HasUserLevels;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface
|
||||
*/
|
||||
private $optionVariableRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
|
||||
*/
|
||||
private $serverRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface
|
||||
*/
|
||||
private $serverVariableRepository;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Validation\Factory
|
||||
*/
|
||||
|
@ -45,20 +28,10 @@ class VariableValidatorService
|
|||
/**
|
||||
* VariableValidatorService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface $optionVariableRepository
|
||||
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository
|
||||
* @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository
|
||||
* @param \Illuminate\Contracts\Validation\Factory $validator
|
||||
*/
|
||||
public function __construct(
|
||||
EggVariableRepositoryInterface $optionVariableRepository,
|
||||
ServerRepositoryInterface $serverRepository,
|
||||
ServerVariableRepositoryInterface $serverVariableRepository,
|
||||
ValidationFactory $validator
|
||||
) {
|
||||
$this->optionVariableRepository = $optionVariableRepository;
|
||||
$this->serverRepository = $serverRepository;
|
||||
$this->serverVariableRepository = $serverVariableRepository;
|
||||
public function __construct(ValidationFactory $validator)
|
||||
{
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
|
@ -72,16 +45,18 @@ class VariableValidatorService
|
|||
*/
|
||||
public function handle(int $egg, array $fields = []): Collection
|
||||
{
|
||||
$variables = $this->optionVariableRepository->findWhere([['egg_id', '=', $egg]]);
|
||||
$query = EggVariable::query()->where('egg_id', $egg);
|
||||
if (! $this->isUserLevel(User::USER_LEVEL_ADMIN)) {
|
||||
// Don't attempt to validate variables if they aren't user editable
|
||||
// and we're not running this at an admin level.
|
||||
$query = $query->where('user_editable', true)->where('user_viewable', true);
|
||||
}
|
||||
|
||||
/** @var \Pterodactyl\Models\EggVariable[] $variables */
|
||||
$variables = $query->get();
|
||||
|
||||
$data = $rules = $customAttributes = [];
|
||||
foreach ($variables as $variable) {
|
||||
// Don't attempt to validate variables if they aren't user editable
|
||||
// and we're not running this at an admin level.
|
||||
if (! $variable->user_editable && ! $this->isUserLevel(User::USER_LEVEL_ADMIN)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data['environment'][$variable->env_variable] = array_get($fields, $variable->env_variable);
|
||||
$rules['environment.' . $variable->env_variable] = $variable->rules;
|
||||
$customAttributes['environment.' . $variable->env_variable] = trans('validation.internal.variable_value', ['env' => $variable->name]);
|
||||
|
@ -92,23 +67,12 @@ class VariableValidatorService
|
|||
throw new ValidationException($validator);
|
||||
}
|
||||
|
||||
$response = $variables->filter(function ($item) {
|
||||
// Skip doing anything if user is not an admin and variable is not user viewable or editable.
|
||||
if (! $this->isUserLevel(User::USER_LEVEL_ADMIN) && (! $item->user_editable || ! $item->user_viewable)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})->map(function ($item) use ($fields) {
|
||||
return (object) [
|
||||
return Collection::make($variables)->map(function ($item) use ($fields) {
|
||||
return (object)[
|
||||
'id' => $item->id,
|
||||
'key' => $item->env_variable,
|
||||
'value' => array_get($fields, $item->env_variable),
|
||||
'value' => $fields[$item->env_variable] ?? null,
|
||||
];
|
||||
})->filter(function ($item) {
|
||||
return is_object($item);
|
||||
});
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ class ServerTransformer extends BaseClientTransformer
|
|||
'allocations' => $server->allocation_limit,
|
||||
'backups' => $server->backup_limit,
|
||||
],
|
||||
'is_suspended' => $server->suspended !== 0,
|
||||
'is_suspended' => $server->suspended,
|
||||
'is_installing' => $server->installed !== 1,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ return [
|
|||
| change this value if you are not maintaining your own internal versions.
|
||||
*/
|
||||
|
||||
'version' => 'canary',
|
||||
'version' => '1.0.1',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
@ -13,7 +13,7 @@ return [
|
|||
*/
|
||||
'rate_limit' => [
|
||||
'client_period' => 1,
|
||||
'client' => env('APP_API_CLIENT_RATELIMIT', 240),
|
||||
'client' => env('APP_API_CLIENT_RATELIMIT', 720),
|
||||
|
||||
'application_period' => 1,
|
||||
'application' => env('APP_API_APPLICATION_RATELIMIT', 240),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
|
||||
return [
|
||||
|
@ -75,5 +76,10 @@ return [
|
|||
'driver' => 'errorlog',
|
||||
'level' => 'debug',
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Cake\Chronos\Chronos;
|
||||
use Illuminate\Support\Str;
|
||||
|
@ -7,6 +8,7 @@ use Pterodactyl\Models\Node;
|
|||
use Faker\Generator as Faker;
|
||||
use Pterodactyl\Models\ApiKey;
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Factory $factory */
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Model Factories
|
||||
|
@ -35,8 +37,8 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker $faker) {
|
|||
'installed' => 1,
|
||||
'database_limit' => null,
|
||||
'allocation_limit' => null,
|
||||
'created_at' => \Carbon\Carbon::now(),
|
||||
'updated_at' => \Carbon\Carbon::now(),
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
];
|
||||
});
|
||||
|
||||
|
@ -160,8 +162,8 @@ $factory->define(Pterodactyl\Models\Database::class, function (Faker $faker) {
|
|||
'username' => str_random(10),
|
||||
'remote' => '%',
|
||||
'password' => $password ?: bcrypt('test123'),
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'created_at' => Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => Carbon::now()->toDateTimeString(),
|
||||
];
|
||||
});
|
||||
|
||||
|
@ -190,7 +192,7 @@ $factory->define(Pterodactyl\Models\ApiKey::class, function (Faker $faker) {
|
|||
'token' => $token ?: $token = encrypt(str_random(Pterodactyl\Models\ApiKey::KEY_LENGTH)),
|
||||
'allowed_ips' => null,
|
||||
'memo' => 'Test Function Key',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'created_at' => Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => Carbon::now()->toDateTimeString(),
|
||||
];
|
||||
});
|
||||
|
|
|
@ -1,12 +1,64 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Collection;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Pterodactyl\Models\Permission as P;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class MergePermissionsTableIntoSubusers extends Migration
|
||||
{
|
||||
/**
|
||||
* A list of all pre-1.0 permissions available to a user and their associated
|
||||
* casting for the new permissions system.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $permissionsMap = [
|
||||
'power-start' => P::ACTION_CONTROL_START,
|
||||
'power-stop' => P::ACTION_CONTROL_STOP,
|
||||
'power-restart' => P::ACTION_CONTROL_RESTART,
|
||||
'power-kill' => P::ACTION_CONTROL_STOP,
|
||||
'send-command' => P::ACTION_CONTROL_CONSOLE,
|
||||
'list-subusers' => P::ACTION_USER_READ,
|
||||
'view-subuser' => P::ACTION_USER_READ,
|
||||
'edit-subuser' => P::ACTION_USER_UPDATE,
|
||||
'create-subuser' => P::ACTION_USER_CREATE,
|
||||
'delete-subuser' => P::ACTION_USER_DELETE,
|
||||
'view-allocations' => P::ACTION_ALLOCATION_READ,
|
||||
'edit-allocation' => P::ACTION_ALLOCATION_UPDATE,
|
||||
'view-startup' => P::ACTION_STARTUP_READ,
|
||||
'edit-startup' => P::ACTION_STARTUP_UPDATE,
|
||||
'view-databases' => P::ACTION_DATABASE_READ,
|
||||
// Better to just break this flow a bit than accidentally grant a dangerous permission.
|
||||
'reset-db-password' => P::ACTION_DATABASE_UPDATE,
|
||||
'delete-database' => P::ACTION_DATABASE_DELETE,
|
||||
'create-database' => P::ACTION_DATABASE_CREATE,
|
||||
'access-sftp' => P::ACTION_FILE_SFTP,
|
||||
'list-files' => P::ACTION_FILE_READ,
|
||||
'edit-files' => P::ACTION_FILE_READ_CONTENT,
|
||||
'save-files' => P::ACTION_FILE_UPDATE,
|
||||
'create-files' => P::ACTION_FILE_CREATE,
|
||||
'delete-files' => P::ACTION_FILE_DELETE,
|
||||
'compress-files' => P::ACTION_FILE_ARCHIVE,
|
||||
'list-schedules' => P::ACTION_SCHEDULE_READ,
|
||||
'view-schedule' => P::ACTION_SCHEDULE_READ,
|
||||
'edit-schedule' => P::ACTION_SCHEDULE_UPDATE,
|
||||
'create-schedule' => P::ACTION_SCHEDULE_CREATE,
|
||||
'delete-schedule' => P::ACTION_SCHEDULE_DELETE,
|
||||
// Skipping these permissions as they are granted if you have more specific read/write permissions.
|
||||
'move-files' => null,
|
||||
'copy-files' => null,
|
||||
'decompress-files' => null,
|
||||
'upload-files' => null,
|
||||
'download-files' => null,
|
||||
// These permissions do not exist in 1.0
|
||||
'toggle-schedule' => null,
|
||||
'queue-schedule' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
|
@ -27,10 +79,19 @@ class MergePermissionsTableIntoSubusers extends Migration
|
|||
|
||||
DB::transaction(function () use (&$cursor) {
|
||||
$cursor->each(function ($datum) {
|
||||
DB::update('UPDATE subusers SET permissions = ? WHERE id = ?', [
|
||||
json_encode(explode(',', $datum->permissions)),
|
||||
$datum->subuser_id,
|
||||
]);
|
||||
$updated = Collection::make(explode(',', $datum->permissions))
|
||||
->map(function ($value) {
|
||||
return self::$permissionsMap[$value] ?? null;
|
||||
})->filter(function ($value) {
|
||||
return !is_null($value) && $value !== Permission::ACTION_WEBSOCKET_CONNECT;
|
||||
})
|
||||
// All subusers get this permission, so make sure it gets pushed into the array.
|
||||
->merge([ Permission::ACTION_WEBSOCKET_CONNECT ])
|
||||
->unique()
|
||||
->values()
|
||||
->toJson();
|
||||
|
||||
DB::update('UPDATE subusers SET permissions = ? WHERE id = ?', [$updated, $datum->subuser_id]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -42,11 +103,15 @@ class MergePermissionsTableIntoSubusers extends Migration
|
|||
*/
|
||||
public function down()
|
||||
{
|
||||
$flipped = array_flip(self::$permissionsMap);
|
||||
|
||||
foreach (DB::select('SELECT id, permissions FROM subusers') as $datum) {
|
||||
$values = [];
|
||||
foreach (json_decode($datum->permissions, true) as $permission) {
|
||||
$values[] = $datum->id;
|
||||
$values[] = $permission;
|
||||
if (!empty($v = $flipped[$permission])) {
|
||||
$values[] = $datum->id;
|
||||
$values[] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($values)) {
|
||||
|
|
|
@ -13,6 +13,10 @@ class AddTableServerTransfers extends Migration
|
|||
*/
|
||||
public function up()
|
||||
{
|
||||
// Nuclear approach to whatever plugins are out there and not properly namespacing their own tables
|
||||
// leading to constant support requests from people...
|
||||
Schema::dropIfExists('server_transfers');
|
||||
|
||||
Schema::create('server_transfers', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('server_id')->unsigned();
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class ChangeUniqueDatabaseNameToAccountForServer extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('databases', function (Blueprint $table) {
|
||||
$table->dropUnique(['database_host_id', 'database']);
|
||||
});
|
||||
|
||||
Schema::table('databases', function (Blueprint $table) {
|
||||
$table->unique(['database_host_id', 'server_id', 'database']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('databases', function (Blueprint $table) {
|
||||
$table->dropUnique(['database_host_id', 'server_id', 'database']);
|
||||
});
|
||||
|
||||
Schema::table('databases', function (Blueprint $table) {
|
||||
$table->unique(['database_host_id', 'database']);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
|
@ -130,7 +130,7 @@ class EggSeeder extends Seeder
|
|||
['nest_id', '=', $nest->id],
|
||||
]);
|
||||
|
||||
$this->updateImporterService->handle($egg->id, $file);
|
||||
$this->updateImporterService->handle($egg, $file);
|
||||
|
||||
$this->command->info('Updated ' . $decoded->name);
|
||||
} catch (RecordNotFoundException $exception) {
|
||||
|
|
|
@ -153,6 +153,12 @@ function updateAdditionalAllocations() {
|
|||
}
|
||||
|
||||
function initUserIdSelect(data) {
|
||||
function escapeHtml(str) {
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(str));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
$('#pUserId').select2({
|
||||
ajax: {
|
||||
url: '/admin/users/accounts.json',
|
||||
|
@ -176,28 +182,27 @@ function initUserIdSelect(data) {
|
|||
data: data,
|
||||
escapeMarkup: function (markup) { return markup; },
|
||||
minimumInputLength: 2,
|
||||
|
||||
templateResult: function (data) {
|
||||
if (data.loading) return data.text;
|
||||
if (data.loading) return escapeHtml(data.text);
|
||||
|
||||
return '<div class="user-block"> \
|
||||
<img class="img-circle img-bordered-xs" src="https://www.gravatar.com/avatar/' + data.md5 + '?s=120" alt="User Image"> \
|
||||
<span class="username"> \
|
||||
<a href="#">' + data.name_first + ' ' + data.name_last +'</a> \
|
||||
</span> \
|
||||
<span class="description"><strong>' + data.email + '</strong> - ' + data.username + '</span> \
|
||||
</div>';
|
||||
<img class="img-circle img-bordered-xs" src="https://www.gravatar.com/avatar/' + escapeHtml(data.md5) + '?s=120" alt="User Image"> \
|
||||
<span class="username"> \
|
||||
<a href="#">' + escapeHtml(data.name_first) + ' ' + escapeHtml(data.name_last) +'</a> \
|
||||
</span> \
|
||||
<span class="description"><strong>' + escapeHtml(data.email) + '</strong> - ' + escapeHtml(data.username) + '</span> \
|
||||
</div>';
|
||||
},
|
||||
|
||||
templateSelection: function (data) {
|
||||
return '<div> \
|
||||
<span> \
|
||||
<img class="img-rounded img-bordered-xs" src="https://www.gravatar.com/avatar/' + data.md5 + '?s=120" style="height:28px;margin-top:-4px;" alt="User Image"> \
|
||||
</span> \
|
||||
<span style="padding-left:5px;"> \
|
||||
' + data.name_first + ' ' + data.name_last + ' (<strong>' + data.email + '</strong>) \
|
||||
</span> \
|
||||
</div>';
|
||||
<span> \
|
||||
<img class="img-rounded img-bordered-xs" src="https://www.gravatar.com/avatar/' + escapeHtml(data.md5) + '?s=120" style="height:28px;margin-top:-4px;" alt="User Image"> \
|
||||
</span> \
|
||||
<span style="padding-left:5px;"> \
|
||||
' + escapeHtml(data.name_first) + ' ' + escapeHtml(data.name_last) + ' (<strong>' + escapeHtml(data.email) + '</strong>) \
|
||||
</span> \
|
||||
</div>';
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ interface Response {
|
|||
}
|
||||
|
||||
export default (uuid: string, initialData?: Response) => useSWR([ uuid, '/startup' ], async (): Promise<Response> => {
|
||||
console.log('firing getServerStartup');
|
||||
const { data } = await http.get(`/api/client/servers/${uuid}/startup`);
|
||||
|
||||
const variables = ((data as FractalResponseList).data || []).map(rawDataToServerEggVariable);
|
||||
|
|
|
@ -94,7 +94,6 @@ export default () => {
|
|||
}
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<Link
|
||||
type={'button'}
|
||||
to={'/auth/login'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||
>
|
||||
|
|
|
@ -14,26 +14,8 @@ import { httpErrorToHuman } from '@/api/http';
|
|||
import { format } from 'date-fns';
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import tw from 'twin.macro';
|
||||
import { breakpoint } from '@/theme';
|
||||
import styled from 'styled-components/macro';
|
||||
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||
|
||||
const Container = styled.div`
|
||||
${tw`flex flex-wrap my-10`};
|
||||
|
||||
& > div {
|
||||
${tw`w-full`};
|
||||
|
||||
${breakpoint('md')`
|
||||
width: calc(50% - 1rem);
|
||||
`}
|
||||
|
||||
${breakpoint('xl')`
|
||||
${tw`w-auto flex-1`};
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
export default () => {
|
||||
const [ deleteIdentifier, setDeleteIdentifier ] = useState('');
|
||||
const [ keys, setKeys ] = useState<ApiKey[]>([]);
|
||||
|
@ -67,12 +49,12 @@ export default () => {
|
|||
|
||||
return (
|
||||
<PageContentBlock title={'Account API'}>
|
||||
<FlashMessageRender byKey={'account'} css={tw`mb-4`}/>
|
||||
<Container>
|
||||
<ContentBox title={'Create API Key'}>
|
||||
<FlashMessageRender byKey={'account'}/>
|
||||
<div css={tw`md:flex flex-no-wrap my-10`}>
|
||||
<ContentBox title={'Create API Key'} css={tw`flex-none w-full md:w-1/2`}>
|
||||
<CreateApiKeyForm onKeyCreated={key => setKeys(s => ([ ...s!, key ]))}/>
|
||||
</ContentBox>
|
||||
<ContentBox title={'API Keys'} css={tw`mt-8 md:mt-0 md:ml-8`}>
|
||||
<ContentBox title={'API Keys'} css={tw`flex-1 overflow-hidden mt-8 md:mt-0 md:ml-8`}>
|
||||
<SpinnerOverlay visible={loading}/>
|
||||
<ConfirmationModal
|
||||
visible={!!deleteIdentifier}
|
||||
|
@ -99,14 +81,14 @@ export default () => {
|
|||
css={[ tw`bg-neutral-600 flex items-center`, index > 0 && tw`mt-2` ]}
|
||||
>
|
||||
<FontAwesomeIcon icon={faKey} css={tw`text-neutral-300`}/>
|
||||
<div css={tw`ml-4 flex-1`}>
|
||||
<p css={tw`text-sm`}>{key.description}</p>
|
||||
<div css={tw`ml-4 flex-1 overflow-hidden`}>
|
||||
<p css={tw`text-sm break-words`}>{key.description}</p>
|
||||
<p css={tw`text-2xs text-neutral-300 uppercase`}>
|
||||
Last used:
|
||||
{key.lastUsedAt ? format(key.lastUsedAt, 'MMM do, yyyy HH:mm') : 'Never'}
|
||||
</p>
|
||||
</div>
|
||||
<p css={tw`text-sm ml-4`}>
|
||||
<p css={tw`text-sm ml-4 hidden md:block`}>
|
||||
<code css={tw`font-mono py-1 px-2 bg-neutral-900 rounded`}>
|
||||
{key.identifier}
|
||||
</code>
|
||||
|
@ -124,7 +106,7 @@ export default () => {
|
|||
))
|
||||
}
|
||||
</ContentBox>
|
||||
</Container>
|
||||
</div>
|
||||
</PageContentBlock>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,21 +1,43 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { memo, useEffect, useRef, useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faEthernet, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Server } from '@/api/server/getServer';
|
||||
import getServerResourceUsage, { ServerStats } from '@/api/server/getServerResourceUsage';
|
||||
import getServerResourceUsage, { ServerPowerState, ServerStats } from '@/api/server/getServerResourceUsage';
|
||||
import { bytesToHuman, megabytesToHuman } from '@/helpers';
|
||||
import tw from 'twin.macro';
|
||||
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import styled from 'styled-components/macro';
|
||||
import isEqual from 'react-fast-compare';
|
||||
|
||||
// Determines if the current value is in an alarm threshold so we can show it in red rather
|
||||
// than the more faded default style.
|
||||
const isAlarmState = (current: number, limit: number): boolean => {
|
||||
const limitInBytes = limit * 1024 * 1024;
|
||||
const isAlarmState = (current: number, limit: number): boolean => limit > 0 && (current / (limit * 1024 * 1024) >= 0.90);
|
||||
|
||||
return current / limitInBytes >= 0.90;
|
||||
};
|
||||
const Icon = memo(styled(FontAwesomeIcon)<{ $alarm: boolean }>`
|
||||
${props => props.$alarm ? tw`text-red-400` : tw`text-neutral-500`};
|
||||
`, isEqual);
|
||||
|
||||
const IconDescription = styled.p<{ $alarm: boolean }>`
|
||||
${tw`text-sm ml-2`};
|
||||
${props => props.$alarm ? tw`text-white` : tw`text-neutral-400`};
|
||||
`;
|
||||
|
||||
const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | undefined }>`
|
||||
${tw`grid grid-cols-12 gap-4 relative`};
|
||||
|
||||
& .status-bar {
|
||||
${tw`w-2 bg-red-500 absolute right-0 z-20 rounded-full m-1 opacity-50 transition-all duration-150`};
|
||||
height: calc(100% - 0.5rem);
|
||||
|
||||
${({ $status }) => (!$status || $status === 'offline') ? tw`bg-red-500` : ($status === 'running' ? tw`bg-green-500` : tw`bg-yellow-500`)};
|
||||
}
|
||||
|
||||
&:hover .status-bar {
|
||||
${tw`opacity-75`};
|
||||
}
|
||||
`;
|
||||
|
||||
export default ({ server, className }: { server: Server; className?: string }) => {
|
||||
const interval = useRef<number>(null);
|
||||
|
@ -54,29 +76,31 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
const memorylimit = server.limits.memory !== 0 ? megabytesToHuman(server.limits.memory) : 'Unlimited';
|
||||
|
||||
return (
|
||||
<GreyRowBox as={Link} to={`/server/${server.id}`} className={className}>
|
||||
<div className={'icon'} css={tw`hidden md:block`}>
|
||||
<FontAwesomeIcon icon={faServer}/>
|
||||
</div>
|
||||
<div css={tw`flex-1 md:ml-4`}>
|
||||
<p css={tw`text-lg break-all`}>{server.name}</p>
|
||||
{!!server.description &&
|
||||
<p css={tw`text-sm text-neutral-300 break-all`}>{server.description}</p>
|
||||
}
|
||||
</div>
|
||||
<div css={tw`w-48 overflow-hidden self-start hidden lg:block`}>
|
||||
<div css={tw`flex ml-4 justify-end`}>
|
||||
<FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`}/>
|
||||
<p css={tw`text-sm text-neutral-400 ml-2`}>
|
||||
{
|
||||
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
|
||||
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span>
|
||||
))
|
||||
}
|
||||
</p>
|
||||
<StatusIndicatorBox as={Link} to={`/server/${server.id}`} className={className} $status={stats?.status}>
|
||||
<div css={tw`flex items-center col-span-12 sm:col-span-5 lg:col-span-6`}>
|
||||
<div className={'icon'} css={tw`mr-4`}>
|
||||
<FontAwesomeIcon icon={faServer}/>
|
||||
</div>
|
||||
<div>
|
||||
<p css={tw`text-lg break-words`}>{server.name}</p>
|
||||
{!!server.description &&
|
||||
<p css={tw`text-sm text-neutral-300 break-words`}>{server.description}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div css={tw`w-1/3 sm:w-1/2 lg:w-1/3 flex items-baseline justify-center relative`}>
|
||||
<div css={tw`hidden lg:col-span-2 lg:flex ml-4 justify-end h-full`}>
|
||||
<FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`}/>
|
||||
<p css={tw`text-sm text-neutral-400 ml-2`}>
|
||||
{
|
||||
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
|
||||
<React.Fragment key={allocation.ip + allocation.port.toString()}>
|
||||
{allocation.alias || allocation.ip}:{allocation.port}
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div css={tw`hidden col-span-7 lg:col-span-4 sm:flex items-baseline justify-center`}>
|
||||
{!stats ?
|
||||
!statsError ?
|
||||
<Spinner size={'small'}/>
|
||||
|
@ -96,80 +120,33 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
:
|
||||
<React.Fragment>
|
||||
<div css={tw`flex-1 flex md:ml-4 sm:flex hidden justify-center`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faMicrochip}
|
||||
css={[
|
||||
!alarms.cpu && tw`text-neutral-500`,
|
||||
alarms.cpu && tw`text-red-400`,
|
||||
]}
|
||||
/>
|
||||
<p
|
||||
css={[
|
||||
tw`text-sm ml-2`,
|
||||
!alarms.cpu && tw`text-neutral-400`,
|
||||
alarms.cpu && tw`text-white`,
|
||||
]}
|
||||
>
|
||||
<Icon icon={faMicrochip} $alarm={alarms.cpu}/>
|
||||
<IconDescription $alarm={alarms.cpu}>
|
||||
{stats.cpuUsagePercent} %
|
||||
</p>
|
||||
</IconDescription>
|
||||
</div>
|
||||
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
||||
<div css={tw`flex justify-center`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faMemory}
|
||||
css={[
|
||||
!alarms.memory && tw`text-neutral-500`,
|
||||
alarms.memory && tw`text-red-400`,
|
||||
]}
|
||||
/>
|
||||
<p
|
||||
css={[
|
||||
tw`text-sm ml-2`,
|
||||
!alarms.memory && tw`text-neutral-400`,
|
||||
alarms.memory && tw`text-white`,
|
||||
]}
|
||||
>
|
||||
<Icon icon={faMemory} $alarm={alarms.memory}/>
|
||||
<IconDescription $alarm={alarms.memory}>
|
||||
{bytesToHuman(stats.memoryUsageInBytes)}
|
||||
</p>
|
||||
</IconDescription>
|
||||
</div>
|
||||
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {memorylimit}</p>
|
||||
</div>
|
||||
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
||||
<div css={tw`flex justify-center`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faHdd}
|
||||
css={[
|
||||
!alarms.disk && tw`text-neutral-500`,
|
||||
alarms.disk && tw`text-red-400`,
|
||||
]}
|
||||
/>
|
||||
<p
|
||||
css={[
|
||||
tw`text-sm ml-2`,
|
||||
!alarms.disk && tw`text-neutral-400`,
|
||||
alarms.disk && tw`text-white`,
|
||||
]}
|
||||
>
|
||||
<Icon icon={faHdd} $alarm={alarms.disk}/>
|
||||
<IconDescription $alarm={alarms.disk}>
|
||||
{bytesToHuman(stats.diskUsageInBytes)}
|
||||
</p>
|
||||
</IconDescription>
|
||||
</div>
|
||||
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {disklimit}</p>
|
||||
</div>
|
||||
<div css={tw`flex-1 flex justify-end sm:hidden`}>
|
||||
<div css={tw`flex items-end text-right`}>
|
||||
<div
|
||||
css={[
|
||||
tw`w-3 h-3 rounded-full`,
|
||||
(!stats?.status || stats?.status === 'offline')
|
||||
? tw`bg-red-500`
|
||||
: (stats?.status === 'running' ? tw`bg-green-500` : tw`bg-yellow-500`),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
</GreyRowBox>
|
||||
<div className={'status-bar'}/>
|
||||
</StatusIndicatorBox>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -69,6 +69,7 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
|
|||
{({ isSubmitting }) => (
|
||||
<Modal
|
||||
{...props}
|
||||
top={false}
|
||||
onDismissed={dismiss}
|
||||
dismissable={!isSubmitting}
|
||||
showSpinnerOverlay={loading || isSubmitting}
|
||||
|
|
|
@ -2,11 +2,11 @@ import styled from 'styled-components/macro';
|
|||
import tw from 'twin.macro';
|
||||
|
||||
export default styled.div<{ $hoverable?: boolean }>`
|
||||
${tw`flex rounded no-underline text-neutral-200 items-center bg-neutral-700 p-4 border border-transparent transition-colors duration-150`};
|
||||
${tw`flex rounded no-underline text-neutral-200 items-center bg-neutral-700 p-4 border border-transparent transition-colors duration-150 overflow-hidden`};
|
||||
|
||||
${props => props.$hoverable !== false && tw`hover:border-neutral-500`};
|
||||
|
||||
& > div.icon {
|
||||
& .icon {
|
||||
${tw`rounded-full bg-neutral-500 p-3`};
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -35,6 +35,8 @@ const ModalContainer = styled.div<{ alignTop?: boolean }>`
|
|||
margin-top: 20%;
|
||||
${breakpoint('md')`margin-top: 10%`};
|
||||
`};
|
||||
|
||||
margin-bottom: auto;
|
||||
|
||||
& > .close-icon {
|
||||
${tw`absolute right-0 p-2 text-white cursor-pointer opacity-50 transition-all duration-150 ease-linear hover:opacity-100`};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { ITerminalOptions, Terminal } from 'xterm';
|
||||
import * as TerminalFit from 'xterm/lib/addons/fit/fit';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
|
@ -7,6 +7,8 @@ import styled from 'styled-components/macro';
|
|||
import { usePermissions } from '@/plugins/usePermissions';
|
||||
import tw from 'twin.macro';
|
||||
import 'xterm/dist/xterm.css';
|
||||
import useEventListener from '@/plugins/useEventListener';
|
||||
import { debounce } from 'debounce';
|
||||
|
||||
const theme = {
|
||||
background: 'transparent',
|
||||
|
@ -51,8 +53,7 @@ const TerminalDiv = styled.div`
|
|||
|
||||
export default () => {
|
||||
const TERMINAL_PRELUDE = '\u001b[1m\u001b[33mcontainer@pterodactyl~ \u001b[0m';
|
||||
const [ terminalElement, setTerminalElement ] = useState<HTMLDivElement | null>(null);
|
||||
const useRef = useCallback(node => setTerminalElement(node), []);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const terminal = useMemo(() => new Terminal({ ...terminalProps }), []);
|
||||
const { connected, instance } = ServerContext.useStoreState(state => state.socket);
|
||||
const [ canSendCommands ] = usePermissions([ 'control.console' ]);
|
||||
|
@ -79,8 +80,8 @@ export default () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (connected && terminalElement && !terminal.element) {
|
||||
terminal.open(terminalElement);
|
||||
if (connected && ref.current && !terminal.element) {
|
||||
terminal.open(ref.current);
|
||||
|
||||
// @see https://github.com/xtermjs/xterm.js/issues/2265
|
||||
// @see https://github.com/xtermjs/xterm.js/issues/2230
|
||||
|
@ -97,7 +98,13 @@ export default () => {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
}, [ terminal, connected, terminalElement ]);
|
||||
}, [ terminal, connected ]);
|
||||
|
||||
const fit = debounce(() => {
|
||||
TerminalFit.fit(terminal);
|
||||
}, 100);
|
||||
|
||||
useEventListener('resize', () => fit());
|
||||
|
||||
useEffect(() => {
|
||||
if (connected && instance) {
|
||||
|
@ -134,7 +141,7 @@ export default () => {
|
|||
maxHeight: '32rem',
|
||||
}}
|
||||
>
|
||||
<TerminalDiv id={'terminal'} ref={useRef}/>
|
||||
<TerminalDiv id={'terminal'} ref={ref}/>
|
||||
</div>
|
||||
{canSendCommands &&
|
||||
<div css={tw`rounded-b bg-neutral-900 text-neutral-100 flex`}>
|
||||
|
|
55
resources/scripts/components/server/PowerControls.tsx
Normal file
55
resources/scripts/components/server/PowerControls.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import Can from '@/components/elements/Can';
|
||||
import Button from '@/components/elements/Button';
|
||||
import StopOrKillButton from '@/components/server/StopOrKillButton';
|
||||
import { PowerAction } from '@/components/server/ServerConsole';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
const PowerControls = () => {
|
||||
const status = ServerContext.useStoreState(state => state.status.value);
|
||||
const instance = ServerContext.useStoreState(state => state.socket.instance);
|
||||
|
||||
const sendPowerCommand = (command: PowerAction) => {
|
||||
instance && instance.send('set state', command);
|
||||
};
|
||||
|
||||
return (
|
||||
<div css={tw`shadow-md bg-neutral-700 rounded p-3 flex text-xs mt-4 justify-center`}>
|
||||
<Can action={'control.start'}>
|
||||
<Button
|
||||
size={'xsmall'}
|
||||
color={'green'}
|
||||
isSecondary
|
||||
css={tw`mr-2`}
|
||||
disabled={status !== 'offline'}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
sendPowerCommand('start');
|
||||
}}
|
||||
>
|
||||
Start
|
||||
</Button>
|
||||
</Can>
|
||||
<Can action={'control.restart'}>
|
||||
<Button
|
||||
size={'xsmall'}
|
||||
isSecondary
|
||||
css={tw`mr-2`}
|
||||
disabled={!status}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
sendPowerCommand('restart');
|
||||
}}
|
||||
>
|
||||
Restart
|
||||
</Button>
|
||||
</Can>
|
||||
<Can action={'control.stop'}>
|
||||
<StopOrKillButton onPress={action => sendPowerCommand(action)}/>
|
||||
</Can>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PowerControls;
|
|
@ -1,130 +1,29 @@
|
|||
import React, { lazy, useEffect, useState } from 'react';
|
||||
import React, { lazy, memo } from 'react';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCircle, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
|
||||
import { bytesToHuman, megabytesToHuman } from '@/helpers';
|
||||
import SuspenseSpinner from '@/components/elements/SuspenseSpinner';
|
||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||
import Can from '@/components/elements/Can';
|
||||
import ContentContainer from '@/components/elements/ContentContainer';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import StopOrKillButton from '@/components/server/StopOrKillButton';
|
||||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||
import ServerDetailsBlock from '@/components/server/ServerDetailsBlock';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import PowerControls from '@/components/server/PowerControls';
|
||||
|
||||
export type PowerAction = 'start' | 'stop' | 'restart' | 'kill';
|
||||
|
||||
const ChunkedConsole = lazy(() => import(/* webpackChunkName: "console" */'@/components/server/Console'));
|
||||
const ChunkedStatGraphs = lazy(() => import(/* webpackChunkName: "graphs" */'@/components/server/StatGraphs'));
|
||||
|
||||
export default () => {
|
||||
const [ memory, setMemory ] = useState(0);
|
||||
const [ cpu, setCpu ] = useState(0);
|
||||
const [ disk, setDisk ] = useState(0);
|
||||
|
||||
const name = ServerContext.useStoreState(state => state.server.data!.name);
|
||||
const limits = ServerContext.useStoreState(state => state.server.data!.limits);
|
||||
const ServerConsole = () => {
|
||||
const isInstalling = ServerContext.useStoreState(state => state.server.data!.isInstalling);
|
||||
const status = ServerContext.useStoreState(state => state.status.value);
|
||||
|
||||
const connected = ServerContext.useStoreState(state => state.socket.connected);
|
||||
const instance = ServerContext.useStoreState(state => state.socket.instance);
|
||||
|
||||
const statsListener = (data: string) => {
|
||||
let stats: any = {};
|
||||
try {
|
||||
stats = JSON.parse(data);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
setMemory(stats.memory_bytes);
|
||||
setCpu(stats.cpu_absolute);
|
||||
setDisk(stats.disk_bytes);
|
||||
};
|
||||
|
||||
const sendPowerCommand = (command: PowerAction) => {
|
||||
instance && instance.send('set state', command);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected || !instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
instance.addListener('stats', statsListener);
|
||||
|
||||
return () => {
|
||||
instance.removeListener('stats', statsListener);
|
||||
};
|
||||
}, [ instance, connected ]);
|
||||
|
||||
const disklimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited';
|
||||
const memorylimit = limits.memory ? megabytesToHuman(limits.memory) : 'Unlimited';
|
||||
|
||||
return (
|
||||
<ServerContentBlock title={'Console'} css={tw`flex flex-wrap`}>
|
||||
<div css={tw`w-full md:w-1/4`}>
|
||||
<TitledGreyBox css={tw`break-all`} title={name} icon={faServer}>
|
||||
<p css={tw`text-xs uppercase`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faCircle}
|
||||
fixedWidth
|
||||
css={[
|
||||
tw`mr-1`,
|
||||
status === 'offline' ? tw`text-red-500` : (status === 'running' ? tw`text-green-500` : tw`text-yellow-500`),
|
||||
]}
|
||||
/>
|
||||
{!status ? 'Connecting...' : status}
|
||||
</p>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
<FontAwesomeIcon icon={faMicrochip} fixedWidth css={tw`mr-1`}/> {cpu.toFixed(2)}%
|
||||
</p>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
<FontAwesomeIcon icon={faMemory} fixedWidth css={tw`mr-1`}/> {bytesToHuman(memory)}
|
||||
<span css={tw`text-neutral-500`}> / {memorylimit}</span>
|
||||
</p>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
<FontAwesomeIcon icon={faHdd} fixedWidth css={tw`mr-1`}/> {bytesToHuman(disk)}
|
||||
<span css={tw`text-neutral-500`}> / {disklimit}</span>
|
||||
</p>
|
||||
</TitledGreyBox>
|
||||
<div css={tw`w-full lg:w-1/4`}>
|
||||
<ServerDetailsBlock/>
|
||||
{!isInstalling ?
|
||||
<Can action={[ 'control.start', 'control.stop', 'control.restart' ]} matchAny>
|
||||
<div css={tw`shadow-md bg-neutral-700 rounded p-3 flex text-xs mt-4 justify-center`}>
|
||||
<Can action={'control.start'}>
|
||||
<Button
|
||||
size={'xsmall'}
|
||||
color={'green'}
|
||||
isSecondary
|
||||
css={tw`mr-2`}
|
||||
disabled={status !== 'offline'}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
sendPowerCommand('start');
|
||||
}}
|
||||
>
|
||||
Start
|
||||
</Button>
|
||||
</Can>
|
||||
<Can action={'control.restart'}>
|
||||
<Button
|
||||
size={'xsmall'}
|
||||
isSecondary
|
||||
css={tw`mr-2`}
|
||||
disabled={!status}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
sendPowerCommand('restart');
|
||||
}}
|
||||
>
|
||||
Restart
|
||||
</Button>
|
||||
</Can>
|
||||
<Can action={'control.stop'}>
|
||||
<StopOrKillButton onPress={action => sendPowerCommand(action)}/>
|
||||
</Can>
|
||||
</div>
|
||||
<PowerControls/>
|
||||
</Can>
|
||||
:
|
||||
<div css={tw`mt-4 rounded bg-yellow-500 p-3`}>
|
||||
|
@ -137,7 +36,7 @@ export default () => {
|
|||
</div>
|
||||
}
|
||||
</div>
|
||||
<div css={tw`w-full md:flex-1 md:ml-4 mt-4 md:mt-0`}>
|
||||
<div css={tw`w-full lg:w-3/4 mt-4 lg:mt-0 lg:pl-4`}>
|
||||
<SuspenseSpinner>
|
||||
<ChunkedConsole/>
|
||||
<ChunkedStatGraphs/>
|
||||
|
@ -146,3 +45,5 @@ export default () => {
|
|||
</ServerContentBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ServerConsole, isEqual);
|
||||
|
|
84
resources/scripts/components/server/ServerDetailsBlock.tsx
Normal file
84
resources/scripts/components/server/ServerDetailsBlock.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import { faCircle, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { bytesToHuman, megabytesToHuman } from '@/helpers';
|
||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
interface Stats {
|
||||
memory: number;
|
||||
cpu: number;
|
||||
disk: number;
|
||||
}
|
||||
|
||||
const ServerDetailsBlock = () => {
|
||||
const [ stats, setStats ] = useState<Stats>({ memory: 0, cpu: 0, disk: 0 });
|
||||
|
||||
const status = ServerContext.useStoreState(state => state.status.value);
|
||||
const connected = ServerContext.useStoreState(state => state.socket.connected);
|
||||
const instance = ServerContext.useStoreState(state => state.socket.instance);
|
||||
|
||||
const statsListener = (data: string) => {
|
||||
let stats: any = {};
|
||||
try {
|
||||
stats = JSON.parse(data);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStats({
|
||||
memory: stats.memory_bytes,
|
||||
cpu: stats.cpu_absolute,
|
||||
disk: stats.disk_bytes,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected || !instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
instance.addListener('stats', statsListener);
|
||||
instance.send('send stats');
|
||||
|
||||
return () => {
|
||||
instance.removeListener('stats', statsListener);
|
||||
};
|
||||
}, [ instance, connected ]);
|
||||
|
||||
const name = ServerContext.useStoreState(state => state.server.data!.name);
|
||||
const limits = ServerContext.useStoreState(state => state.server.data!.limits);
|
||||
|
||||
const disklimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited';
|
||||
const memorylimit = limits.memory ? megabytesToHuman(limits.memory) : 'Unlimited';
|
||||
|
||||
return (
|
||||
<TitledGreyBox css={tw`break-words`} title={name} icon={faServer}>
|
||||
<p css={tw`text-xs uppercase`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faCircle}
|
||||
fixedWidth
|
||||
css={[
|
||||
tw`mr-1`,
|
||||
status === 'offline' ? tw`text-red-500` : (status === 'running' ? tw`text-green-500` : tw`text-yellow-500`),
|
||||
]}
|
||||
/>
|
||||
{!status ? 'Connecting...' : status}
|
||||
</p>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
<FontAwesomeIcon icon={faMicrochip} fixedWidth css={tw`mr-1`}/> {stats.cpu.toFixed(2)}%
|
||||
</p>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
<FontAwesomeIcon icon={faMemory} fixedWidth css={tw`mr-1`}/> {bytesToHuman(stats.memory)}
|
||||
<span css={tw`text-neutral-500`}> / {memorylimit}</span>
|
||||
</p>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
<FontAwesomeIcon icon={faHdd} fixedWidth css={tw`mr-1`}/> {bytesToHuman(stats.disk)}
|
||||
<span css={tw`text-neutral-500`}> / {disklimit}</span>
|
||||
</p>
|
||||
</TitledGreyBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerDetailsBlock;
|
|
@ -142,24 +142,28 @@ export default () => {
|
|||
|
||||
return (
|
||||
<div css={tw`flex flex-wrap mt-4`}>
|
||||
<TitledGreyBox title={'Memory usage'} icon={faMemory} css={tw`md:flex-1 w-full md:w-1/2 md:mr-2`}>
|
||||
{status !== 'offline' ?
|
||||
<canvas id={'memory_chart'} ref={memoryRef} aria-label={'Server Memory Usage Graph'} role={'img'}/>
|
||||
:
|
||||
<p css={tw`text-xs text-neutral-400 text-center p-3`}>
|
||||
Server is offline.
|
||||
</p>
|
||||
}
|
||||
</TitledGreyBox>
|
||||
<TitledGreyBox title={'CPU usage'} icon={faMicrochip} css={tw`md:flex-1 w-full md:w-1/2 md:ml-2 mt-4 md:mt-0`}>
|
||||
{status !== 'offline' ?
|
||||
<canvas id={'cpu_chart'} ref={cpuRef} aria-label={'Server CPU Usage Graph'} role={'img'}/>
|
||||
:
|
||||
<p css={tw`text-xs text-neutral-400 text-center p-3`}>
|
||||
Server is offline.
|
||||
</p>
|
||||
}
|
||||
</TitledGreyBox>
|
||||
<div css={tw`w-full sm:w-1/2`}>
|
||||
<TitledGreyBox title={'Memory usage'} icon={faMemory} css={tw`mr-0 sm:mr-4`}>
|
||||
{status !== 'offline' ?
|
||||
<canvas id={'memory_chart'} ref={memoryRef} aria-label={'Server Memory Usage Graph'} role={'img'}/>
|
||||
:
|
||||
<p css={tw`text-xs text-neutral-400 text-center p-3`}>
|
||||
Server is offline.
|
||||
</p>
|
||||
}
|
||||
</TitledGreyBox>
|
||||
</div>
|
||||
<div css={tw`w-full sm:w-1/2 mt-4 sm:mt-0`}>
|
||||
<TitledGreyBox title={'CPU usage'} icon={faMicrochip} css={tw`ml-0 sm:ml-4`}>
|
||||
{status !== 'offline' ?
|
||||
<canvas id={'cpu_chart'} ref={cpuRef} aria-label={'Server CPU Usage Graph'} role={'img'}/>
|
||||
:
|
||||
<p css={tw`text-xs text-neutral-400 text-center p-3`}>
|
||||
Server is offline.
|
||||
</p>
|
||||
}
|
||||
</TitledGreyBox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,20 +8,27 @@ import Spinner from '@/components/elements/Spinner';
|
|||
import tw from 'twin.macro';
|
||||
|
||||
export default () => {
|
||||
const server = ServerContext.useStoreState(state => state.server.data);
|
||||
const [ error, setError ] = useState(false);
|
||||
let updatingToken = false;
|
||||
const [ error, setError ] = useState<'connecting' | string>('');
|
||||
const { connected, instance } = ServerContext.useStoreState(state => state.socket);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
|
||||
const setServerStatus = ServerContext.useStoreActions(actions => actions.status.setServerStatus);
|
||||
const { setInstance, setConnectionState } = ServerContext.useStoreActions(actions => actions.socket);
|
||||
|
||||
const updateToken = (uuid: string, socket: Websocket) => {
|
||||
if (updatingToken) return;
|
||||
|
||||
updatingToken = true;
|
||||
getWebsocketToken(uuid)
|
||||
.then(data => socket.setToken(data.token, true))
|
||||
.catch(error => console.error(error));
|
||||
.catch(error => console.error(error))
|
||||
.then(() => {
|
||||
updatingToken = false;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
connected && setError(false);
|
||||
connected && setError('');
|
||||
}, [ connected ]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -33,7 +40,7 @@ export default () => {
|
|||
useEffect(() => {
|
||||
// If there is already an instance or there is no server, just exit out of this process
|
||||
// since we don't need to make a new connection.
|
||||
if (instance || !server) {
|
||||
if (instance || !uuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -42,7 +49,7 @@ export default () => {
|
|||
socket.on('auth success', () => setConnectionState(true));
|
||||
socket.on('SOCKET_CLOSE', () => setConnectionState(false));
|
||||
socket.on('SOCKET_ERROR', () => {
|
||||
setError(true);
|
||||
setError('connecting');
|
||||
setConnectionState(false);
|
||||
});
|
||||
socket.on('status', (status) => setServerStatus(status));
|
||||
|
@ -51,10 +58,20 @@ export default () => {
|
|||
console.warn('Got error message from daemon socket:', message);
|
||||
});
|
||||
|
||||
socket.on('token expiring', () => updateToken(server.uuid, socket));
|
||||
socket.on('token expired', () => updateToken(server.uuid, socket));
|
||||
socket.on('token expiring', () => updateToken(uuid, socket));
|
||||
socket.on('token expired', () => updateToken(uuid, socket));
|
||||
socket.on('jwt error', (error: string) => {
|
||||
setConnectionState(false);
|
||||
console.warn('JWT validation error from wings:', error);
|
||||
|
||||
getWebsocketToken(server.uuid)
|
||||
if (error === 'jwt: exp claim is invalid') {
|
||||
updateToken(uuid, socket);
|
||||
} else {
|
||||
setError('There was an error validating the credentials provided for the websocket. Please refresh the page.');
|
||||
}
|
||||
});
|
||||
|
||||
getWebsocketToken(uuid)
|
||||
.then(data => {
|
||||
// Connect and then set the authentication token.
|
||||
socket.setToken(data.token).connect(data.socket);
|
||||
|
@ -63,17 +80,25 @@ export default () => {
|
|||
setInstance(socket);
|
||||
})
|
||||
.catch(error => console.error(error));
|
||||
}, [ server ]);
|
||||
}, [ uuid ]);
|
||||
|
||||
return (
|
||||
error ?
|
||||
<CSSTransition timeout={150} in appear classNames={'fade'}>
|
||||
<div css={tw`bg-red-500 py-2`}>
|
||||
<ContentContainer css={tw`flex items-center justify-center`}>
|
||||
<Spinner size={'small'}/>
|
||||
<p css={tw`ml-2 text-sm text-red-100`}>
|
||||
We're having some trouble connecting to your server, please wait...
|
||||
</p>
|
||||
{error === 'connecting' ?
|
||||
<>
|
||||
<Spinner size={'small'}/>
|
||||
<p css={tw`ml-2 text-sm text-red-100`}>
|
||||
We're having some trouble connecting to your server, please wait...
|
||||
</p>
|
||||
</>
|
||||
:
|
||||
<p css={tw`ml-2 text-sm text-red-100`}>
|
||||
{error}
|
||||
</p>
|
||||
}
|
||||
</ContentContainer>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
|
|
|
@ -40,31 +40,35 @@ export default ({ backup, className }: Props) => {
|
|||
});
|
||||
|
||||
return (
|
||||
<GreyRowBox css={tw`flex items-center`} className={className}>
|
||||
<div css={tw`mr-4`}>
|
||||
{backup.completedAt ?
|
||||
<FontAwesomeIcon icon={faArchive} css={tw`text-neutral-300 hidden md:block`}/>
|
||||
:
|
||||
<Spinner size={'small'}/>
|
||||
}
|
||||
</div>
|
||||
<div css={tw`flex-1`}>
|
||||
<p css={tw`text-sm mb-1`}>
|
||||
{!backup.isSuccessful &&
|
||||
<span css={tw`bg-red-500 py-px px-2 rounded-full text-white text-xs uppercase border border-red-600 mr-2`}>
|
||||
Failed
|
||||
</span>
|
||||
<GreyRowBox css={tw`flex-wrap md:flex-no-wrap items-center`} className={className}>
|
||||
<div css={tw`flex items-center truncate w-full md:flex-1`}>
|
||||
<div css={tw`mr-4`}>
|
||||
{backup.completedAt ?
|
||||
<FontAwesomeIcon icon={faArchive} css={tw`text-neutral-300`}/>
|
||||
:
|
||||
<Spinner size={'small'}/>
|
||||
}
|
||||
{backup.name}
|
||||
{(backup.completedAt && backup.isSuccessful) &&
|
||||
<span css={tw`ml-3 text-neutral-300 text-xs font-thin hidden sm:inline`}>{bytesToHuman(backup.bytes)}</span>
|
||||
}
|
||||
</p>
|
||||
<p css={tw`text-xs text-neutral-400 font-mono hidden md:block`}>
|
||||
{backup.uuid}
|
||||
</p>
|
||||
</div>
|
||||
<div css={tw`flex flex-col truncate`}>
|
||||
<div css={tw`flex items-center text-sm mb-1`}>
|
||||
{!backup.isSuccessful &&
|
||||
<span css={tw`bg-red-500 py-px px-2 rounded-full text-white text-xs uppercase border border-red-600 mr-2`}>
|
||||
Failed
|
||||
</span>
|
||||
}
|
||||
<p css={tw`break-words truncate`}>
|
||||
{backup.name}
|
||||
</p>
|
||||
{(backup.completedAt && backup.isSuccessful) &&
|
||||
<span css={tw`ml-3 text-neutral-300 text-xs font-thin hidden sm:inline`}>{bytesToHuman(backup.bytes)}</span>
|
||||
}
|
||||
</div>
|
||||
<p css={tw`mt-1 md:mt-0 text-xs text-neutral-400 font-mono truncate`}>
|
||||
{backup.uuid}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div css={tw`ml-8 text-center`}>
|
||||
<div css={tw`flex-1 md:flex-none md:w-48 mt-4 md:mt-0 md:ml-8 md:text-center`}>
|
||||
<p
|
||||
title={format(backup.createdAt, 'ddd, MMMM do, yyyy HH:mm:ss')}
|
||||
css={tw`text-sm`}
|
||||
|
@ -74,7 +78,7 @@ export default ({ backup, className }: Props) => {
|
|||
<p css={tw`text-2xs text-neutral-500 uppercase mt-1`}>Created</p>
|
||||
</div>
|
||||
<Can action={'backup.download'}>
|
||||
<div css={tw`ml-6`} style={{ marginRight: '-0.5rem' }}>
|
||||
<div css={tw`mt-4 md:mt-0 ml-6`} style={{ marginRight: '-0.5rem' }}>
|
||||
{!backup.completedAt ?
|
||||
<div css={tw`p-2 invisible`}>
|
||||
<FontAwesomeIcon icon={faEllipsisH}/>
|
||||
|
|
|
@ -87,14 +87,14 @@ export default () => {
|
|||
onSubmit={submit}
|
||||
initialValues={{ name: '', ignored: '' }}
|
||||
validationSchema={object().shape({
|
||||
name: string().max(255),
|
||||
name: string().max(191),
|
||||
ignored: string(),
|
||||
})}
|
||||
>
|
||||
<ModalContent appear visible={visible} onDismissed={() => setVisible(false)}/>
|
||||
</Formik>
|
||||
}
|
||||
<Button onClick={() => setVisible(true)}>
|
||||
<Button css={tw`w-full sm:w-auto`} onClick={() => setVisible(true)}>
|
||||
Create backup
|
||||
</Button>
|
||||
</>
|
||||
|
|
|
@ -32,6 +32,7 @@ export default () => {
|
|||
|
||||
const id = ServerContext.useStoreState(state => state.server.data!.id);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
|
||||
const { addError, clearFlashes } = useFlash();
|
||||
|
||||
let fetchFileContent: null | (() => Promise<string>) = null;
|
||||
|
@ -39,8 +40,9 @@ export default () => {
|
|||
useEffect(() => {
|
||||
if (action === 'new') return;
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setLoading(true);
|
||||
setDirectory(hash.replace(/^#/, '').split('/').filter(v => !!v).slice(0, -1).join('/'));
|
||||
getFileContents(uuid, hash.replace(/^#/, ''))
|
||||
.then(setContent)
|
||||
.catch(error => {
|
||||
|
@ -116,7 +118,13 @@ export default () => {
|
|||
fetchContent={value => {
|
||||
fetchFileContent = value;
|
||||
}}
|
||||
onContentSaved={save}
|
||||
onContentSaved={() => {
|
||||
if (action !== 'edit') {
|
||||
setModalVisible(true);
|
||||
} else {
|
||||
save();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`flex justify-end mt-4`}>
|
||||
|
|
|
@ -45,13 +45,15 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
|
|||
|
||||
return (
|
||||
<div css={tw`flex items-center text-sm mb-4 text-neutral-500`}>
|
||||
{(files && files.length > 0 && !params?.action) &&
|
||||
<FileActionCheckbox
|
||||
type={'checkbox'}
|
||||
css={tw`mx-4`}
|
||||
checked={selectedFilesLength === (files ? files.length : -1)}
|
||||
onChange={onSelectAllClick}
|
||||
/>
|
||||
{(files && files.length > 0 && !params?.action) ?
|
||||
<FileActionCheckbox
|
||||
type={'checkbox'}
|
||||
css={tw`mx-4`}
|
||||
checked={selectedFilesLength === (files ? files.length : -1)}
|
||||
onChange={onSelectAllClick}
|
||||
/>
|
||||
:
|
||||
<div css={tw`w-12`}/>
|
||||
}
|
||||
/<span css={tw`px-1 text-neutral-300`}>home</span>/
|
||||
<NavLink
|
||||
|
|
|
@ -38,13 +38,13 @@ const Clickable: React.FC<{ file: FileObject }> = memo(({ file, children }) => {
|
|||
|
||||
return (
|
||||
(!canReadContents || (file.isFile && !file.isEditable())) ?
|
||||
<div css={tw`flex flex-1 text-neutral-300 no-underline p-3 cursor-default`}>
|
||||
<div css={tw`flex flex-1 text-neutral-300 no-underline p-3 cursor-default overflow-hidden truncate`}>
|
||||
{children}
|
||||
</div>
|
||||
:
|
||||
<NavLink
|
||||
to={`${match.url}/${file.isFile ? 'edit/' : ''}#${cleanDirectoryPath(`${directory}/${file.name}`)}`}
|
||||
css={tw`flex flex-1 text-neutral-300 no-underline p-3`}
|
||||
css={tw`flex flex-1 text-neutral-300 no-underline p-3 overflow-hidden truncate`}
|
||||
onClick={onRowClick}
|
||||
>
|
||||
{children}
|
||||
|
@ -69,7 +69,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => (
|
|||
<FontAwesomeIcon icon={faFolder}/>
|
||||
}
|
||||
</div>
|
||||
<div css={tw`flex-1`}>
|
||||
<div css={tw`flex-1 truncate`}>
|
||||
{file.name}
|
||||
</div>
|
||||
{file.isFile &&
|
||||
|
@ -92,4 +92,11 @@ const FileObjectRow = ({ file }: { file: FileObject }) => (
|
|||
</Row>
|
||||
);
|
||||
|
||||
export default memo(FileObjectRow, isEqual);
|
||||
export default memo(FileObjectRow, (prevProps, nextProps) => {
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
const { isArchiveType, isEditable, ...prevFile } = prevProps.file;
|
||||
const { isArchiveType: nextIsArchiveType, isEditable: nextIsEditable, ...nextFile } = nextProps.file;
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
|
||||
return isEqual(prevFile, nextFile);
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue