Merge branch 'develop' into feature/react-admin
This commit is contained in:
commit
a3b59f24af
95 changed files with 2671 additions and 1777 deletions
|
@ -15,6 +15,7 @@ class UpgradeCommand extends Command
|
|||
/** @var string */
|
||||
protected $signature = 'p:upgrade
|
||||
{--user= : The user that PHP runs under. All files will be owned by this user.}
|
||||
{--group= : The group that PHP runs under. All files will be owned by this group.}
|
||||
{--url= : The specific archive to download.}
|
||||
{--release= : A specific Pterodactyl version to download from GitHub. Leave blank to use latest.}
|
||||
{--skip-download : If set no archive will be downloaded.}';
|
||||
|
@ -46,22 +47,39 @@ class UpgradeCommand extends Command
|
|||
}
|
||||
|
||||
$user = 'www-data';
|
||||
$group = 'www-data';
|
||||
if ($this->input->isInteractive()) {
|
||||
if (!$skipDownload) {
|
||||
$skipDownload = !$this->confirm('Would you like to download and unpack the archive files for the latest version?', true);
|
||||
}
|
||||
|
||||
if (is_null($this->option('user'))) {
|
||||
$details = posix_getpwuid(fileowner('public'));
|
||||
$user = $details['name'] ?? 'www-data';
|
||||
$userDetails = posix_getpwuid(fileowner('public'));
|
||||
$user = $userDetails['name'] ?? 'www-data';
|
||||
|
||||
if (!$this->confirm("Your webserver user has been detected as [{$user}]: is this correct?", true)) {
|
||||
$user = $this->anticipate(
|
||||
'Please enter the name of the user running your webserver process. This varies from system to system, but is generally "www-data", "nginx", or "apache".',
|
||||
[
|
||||
'www-data',
|
||||
'apache',
|
||||
'nginx',
|
||||
'apache',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($this->option('group'))) {
|
||||
$groupDetails = posix_getgrgid(filegroup('public'));
|
||||
$group = $groupDetails['name'] ?? 'www-data';
|
||||
|
||||
if (!$this->confirm("Your webserver group has been detected as [{$group}]: is this correct?", true)) {
|
||||
$group = $this->anticipate(
|
||||
'Please enter the name of the group running your webserver process. Normally this is the same as your user.',
|
||||
[
|
||||
'www-data',
|
||||
'nginx',
|
||||
'apache',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -136,9 +154,9 @@ class UpgradeCommand extends Command
|
|||
$this->call('migrate', ['--seed' => '', '--force' => '']);
|
||||
});
|
||||
|
||||
$this->withProgress($bar, function () use ($user) {
|
||||
$this->line("\$upgrader> chown -R {$user}:{$user} *");
|
||||
$process = Process::fromShellCommandline("chown -R {$user}:{$user} *", $this->getLaravel()->basePath());
|
||||
$this->withProgress($bar, function () use ($user, $group) {
|
||||
$this->line("\$upgrader> chown -R {$user}:{$group} *");
|
||||
$process = Process::fromShellCommandline("chown -R {$user}:{$group} *", $this->getLaravel()->basePath());
|
||||
$process->setTimeout(10 * 60);
|
||||
$process->run(function ($type, $buffer) {
|
||||
$this->{$type === Process::ERR ? 'error' : 'line'}($buffer);
|
||||
|
|
|
@ -38,6 +38,15 @@ class DaemonConnectionException extends DisplayException
|
|||
|
||||
if ($useStatusCode) {
|
||||
$this->statusCode = is_null($response) ? $this->statusCode : $response->getStatusCode();
|
||||
// There are rare conditions where wings encounters a panic condition and crashes the
|
||||
// request being made after content has already been sent over the wire. In these cases
|
||||
// you can end up with a "successful" response code that is actual an error.
|
||||
//
|
||||
// Handle those better here since we shouldn't ever end up in this exception state and
|
||||
// be returning a 2XX level response.
|
||||
if ($this->statusCode < 400) {
|
||||
$this->statusCode = Response::HTTP_BAD_GATEWAY;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($response)) {
|
||||
|
|
16
app/Exceptions/Service/Backup/BackupLockedException.php
Normal file
16
app/Exceptions/Service/Backup/BackupLockedException.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Exceptions\Service\Backup;
|
||||
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
|
||||
class BackupLockedException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* TooManyBackupsException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('Cannot delete a backup that is marked as locked.');
|
||||
}
|
||||
}
|
|
@ -108,7 +108,7 @@ class NodeController extends ApplicationApiController
|
|||
$node = $this->updateService->handle(
|
||||
$node,
|
||||
$request->validated(),
|
||||
$request->input('reset_secret')
|
||||
$request->input('reset_secret'),
|
||||
);
|
||||
|
||||
return $this->fractal->item($node)
|
||||
|
|
|
@ -35,7 +35,7 @@ class NodeDeploymentController extends ApplicationApiController
|
|||
$nodes = $this->viableNodesService->setLocations($data['location_ids'] ?? [])
|
||||
->setMemory($data['memory'])
|
||||
->setDisk($data['disk'])
|
||||
->handle($request->input('page') ?? 0);
|
||||
->handle($request->query('per_page'), $request->query('page'));
|
||||
|
||||
return $this->fractal->collection($nodes)
|
||||
->transformWith($this->getTransformer(NodeTransformer::class))
|
||||
|
|
|
@ -72,11 +72,18 @@ class BackupController extends ClientApiController
|
|||
{
|
||||
/** @var \Pterodactyl\Models\Backup $backup */
|
||||
$backup = $server->audit(AuditLog::SERVER__BACKUP_STARTED, function (AuditLog $model, Server $server) use ($request) {
|
||||
$backup = $this->initiateBackupService
|
||||
->setIgnoredFiles(
|
||||
explode(PHP_EOL, $request->input('ignored') ?? '')
|
||||
)
|
||||
->handle($server, $request->input('name'));
|
||||
$action = $this->initiateBackupService
|
||||
->setIgnoredFiles(explode(PHP_EOL, $request->input('ignored') ?? ''));
|
||||
|
||||
// Only set the lock status if the user even has permission to delete backups,
|
||||
// otherwise ignore this status. This gets a little funky since it isn't clear
|
||||
// how best to allow a user to create a backup that is locked without also preventing
|
||||
// them from just filling up a server with backups that can never be deleted?
|
||||
if ($request->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
|
||||
$action->setIsLocked((bool) $request->input('is_locked'));
|
||||
}
|
||||
|
||||
$backup = $action->handle($server, $request->input('name'));
|
||||
|
||||
$model->metadata = ['backup_uuid' => $backup->uuid];
|
||||
|
||||
|
@ -88,6 +95,32 @@ class BackupController extends ClientApiController
|
|||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the lock status of a given backup for a server.
|
||||
*
|
||||
* @throws \Throwable
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function toggleLock(Request $request, Server $server, Backup $backup): array
|
||||
{
|
||||
if (!$request->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
|
||||
throw new AuthorizationException();
|
||||
}
|
||||
|
||||
$action = $backup->is_locked ? AuditLog::SERVER__BACKUP_UNLOCKED : AuditLog::SERVER__BACKUP_LOCKED;
|
||||
$server->audit($action, function (AuditLog $audit) use ($backup) {
|
||||
$audit->metadata = ['backup_uuid' => $backup->uuid];
|
||||
|
||||
$backup->update(['is_locked' => !$backup->is_locked]);
|
||||
});
|
||||
|
||||
$backup->refresh();
|
||||
|
||||
return $this->fractal->item($backup)
|
||||
->transformWith($this->getTransformer(BackupTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns information about a single backup.
|
||||
*
|
||||
|
|
|
@ -36,11 +36,10 @@ class ResourceUtilizationController extends ClientApiController
|
|||
*/
|
||||
public function __invoke(GetServerRequest $request, Server $server): array
|
||||
{
|
||||
$stats = $this->cache
|
||||
->tags(['resources'])
|
||||
->remember($server->uuid, Carbon::now()->addSeconds(20), function () use ($server) {
|
||||
return $this->repository->setServer($server)->getDetails();
|
||||
});
|
||||
$key = "resources:{$server->uuid}";
|
||||
$stats = $this->cache->remember($key, Carbon::now()->addSeconds(20), function () use ($server) {
|
||||
return $this->repository->setServer($server)->getDetails();
|
||||
});
|
||||
|
||||
return $this->fractal->item($stats)
|
||||
->transformWith($this->getTransformer(StatsTransformer::class))
|
||||
|
|
|
@ -15,7 +15,6 @@ use Pterodactyl\Services\Schedules\ProcessScheduleService;
|
|||
use Pterodactyl\Transformers\Api\Client\ScheduleTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\ViewScheduleRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreScheduleRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\DeleteScheduleRequest;
|
||||
|
@ -72,6 +71,7 @@ class ScheduleController extends ClientApiController
|
|||
'cron_hour' => $request->input('hour'),
|
||||
'cron_minute' => $request->input('minute'),
|
||||
'is_active' => (bool) $request->input('is_active'),
|
||||
'only_when_online' => (bool) $request->input('only_when_online'),
|
||||
'next_run_at' => $this->getNextRunAt($request),
|
||||
]);
|
||||
|
||||
|
@ -118,6 +118,7 @@ class ScheduleController extends ClientApiController
|
|||
'cron_hour' => $request->input('hour'),
|
||||
'cron_minute' => $request->input('minute'),
|
||||
'is_active' => $active,
|
||||
'only_when_online' => (bool) $request->input('only_when_online'),
|
||||
'next_run_at' => $this->getNextRunAt($request),
|
||||
];
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ class ScheduleTaskController extends ClientApiController
|
|||
/**
|
||||
* Create a new task for a given schedule and store it in the database.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\HttpForbiddenException
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Service\ServiceLimitExceededException
|
||||
* @throws \Illuminate\Contracts\Container\BindingResolutionException
|
||||
|
@ -44,6 +45,10 @@ class ScheduleTaskController extends ClientApiController
|
|||
throw new ServiceLimitExceededException("Schedules may not have more than {$limit} tasks associated with them. Creating this task would put this schedule over the limit.");
|
||||
}
|
||||
|
||||
if ($server->backup_limit === 0 && $request->action === 'backup') {
|
||||
throw new HttpForbiddenException("A backup task cannot be created when the server's backup limit is set to 0.");
|
||||
}
|
||||
|
||||
/** @var \Pterodactyl\Models\Task|null $lastTask */
|
||||
$lastTask = $schedule->tasks()->orderByDesc('sequence_id')->first();
|
||||
|
||||
|
@ -54,6 +59,7 @@ class ScheduleTaskController extends ClientApiController
|
|||
'action' => $request->input('action'),
|
||||
'payload' => $request->input('payload') ?? '',
|
||||
'time_offset' => $request->input('time_offset'),
|
||||
'continue_on_failure' => (bool) $request->input('continue_on_failure'),
|
||||
]);
|
||||
|
||||
return $this->fractal->item($task)
|
||||
|
@ -64,6 +70,7 @@ class ScheduleTaskController extends ClientApiController
|
|||
/**
|
||||
* Updates a given task for a server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\HttpForbiddenException
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @throws \Illuminate\Contracts\Container\BindingResolutionException
|
||||
|
@ -74,10 +81,15 @@ class ScheduleTaskController extends ClientApiController
|
|||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
if ($server->backup_limit === 0 && $request->action === 'backup') {
|
||||
throw new HttpForbiddenException("A backup task cannot be created when the server's backup limit is set to 0.");
|
||||
}
|
||||
|
||||
$this->repository->update($task->id, [
|
||||
'action' => $request->input('action'),
|
||||
'payload' => $request->input('payload') ?? '',
|
||||
'time_offset' => $request->input('time_offset'),
|
||||
'continue_on_failure' => (bool) $request->input('continue_on_failure'),
|
||||
]);
|
||||
|
||||
return $this->fractal->item($task->refresh())
|
||||
|
|
|
@ -90,7 +90,7 @@ class ServerDetailsController extends Controller
|
|||
/** @var \Pterodactyl\Models\Server[] $servers */
|
||||
$servers = Server::query()
|
||||
->select('servers.*')
|
||||
->selectRaw('started.metadata->>"$.backup_uuid" as backup_uuid')
|
||||
->selectRaw('JSON_UNQUOTE(JSON_EXTRACT(started.metadata, "$.backup_uuid")) as backup_uuid')
|
||||
->leftJoinSub(function (Builder $builder) {
|
||||
$builder->select('*')->from('audit_logs')
|
||||
->where('action', AuditLog::SERVER__BACKUP_RESTORE_STARTED)
|
||||
|
|
|
@ -19,6 +19,7 @@ class StoreBackupRequest extends ClientApiRequest
|
|||
{
|
||||
return [
|
||||
'name' => 'nullable|string|max:191',
|
||||
'is_locked' => 'nullable|boolean',
|
||||
'ignored' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ use Pterodactyl\Jobs\Job;
|
|||
use Carbon\CarbonImmutable;
|
||||
use Pterodactyl\Models\Task;
|
||||
use InvalidArgumentException;
|
||||
use Pterodactyl\Models\Schedule;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
@ -15,6 +14,7 @@ use Illuminate\Foundation\Bus\DispatchesJobs;
|
|||
use Pterodactyl\Services\Backups\InitiateBackupService;
|
||||
use Pterodactyl\Repositories\Wings\DaemonPowerRepository;
|
||||
use Pterodactyl\Repositories\Wings\DaemonCommandRepository;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
class RunTaskJob extends Job implements ShouldQueue
|
||||
{
|
||||
|
@ -27,13 +27,19 @@ class RunTaskJob extends Job implements ShouldQueue
|
|||
*/
|
||||
public $task;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $manualRun;
|
||||
|
||||
/**
|
||||
* RunTaskJob constructor.
|
||||
*/
|
||||
public function __construct(Task $task)
|
||||
public function __construct(Task $task, $manualRun = false)
|
||||
{
|
||||
$this->queue = config('pterodactyl.queues.standard');
|
||||
$this->task = $task;
|
||||
$this->manualRun = $manualRun;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,8 +52,8 @@ class RunTaskJob extends Job implements ShouldQueue
|
|||
InitiateBackupService $backupService,
|
||||
DaemonPowerRepository $powerRepository
|
||||
) {
|
||||
// Do not process a task that is not set to active.
|
||||
if (!$this->task->schedule->is_active) {
|
||||
// Do not process a task that is not set to active, unless it's been manually triggered.
|
||||
if (!$this->task->schedule->is_active && !$this->manualRun) {
|
||||
$this->markTaskNotQueued();
|
||||
$this->markScheduleComplete();
|
||||
|
||||
|
@ -56,18 +62,26 @@ class RunTaskJob extends Job implements ShouldQueue
|
|||
|
||||
$server = $this->task->server;
|
||||
// Perform the provided task against the daemon.
|
||||
switch ($this->task->action) {
|
||||
case 'power':
|
||||
$powerRepository->setServer($server)->send($this->task->payload);
|
||||
break;
|
||||
case 'command':
|
||||
$commandRepository->setServer($server)->send($this->task->payload);
|
||||
break;
|
||||
case 'backup':
|
||||
$backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null, true);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException('Cannot run a task that points to a non-existent action.');
|
||||
try {
|
||||
switch ($this->task->action) {
|
||||
case Task::ACTION_POWER:
|
||||
$powerRepository->setServer($server)->send($this->task->payload);
|
||||
break;
|
||||
case Task::ACTION_COMMAND:
|
||||
$commandRepository->setServer($server)->send($this->task->payload);
|
||||
break;
|
||||
case Task::ACTION_BACKUP:
|
||||
$backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null, true);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid task action provided: ' . $this->task->action);
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
// If this isn't a DaemonConnectionException on a task that allows for failures
|
||||
// throw the exception back up the chain so that the task is stopped.
|
||||
if (!($this->task->continue_on_failure && $exception instanceof DaemonConnectionException)) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
$this->markTaskNotQueued();
|
||||
|
@ -101,7 +115,7 @@ class RunTaskJob extends Job implements ShouldQueue
|
|||
|
||||
$nextTask->update(['is_queued' => true]);
|
||||
|
||||
$this->dispatch((new self($nextTask))->delay($nextTask->time_offset));
|
||||
$this->dispatch((new self($nextTask, $this->manualRun))->delay($nextTask->time_offset));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,17 +7,17 @@ use Illuminate\Http\Request;
|
|||
use Illuminate\Container\Container;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $uuid
|
||||
* @property bool $is_system
|
||||
* @property int|null $user_id
|
||||
* @property int|null $server_id
|
||||
* @property string $action
|
||||
* @property string|null $subaction
|
||||
* @property array $device
|
||||
* @property array $metadata
|
||||
* @property \Carbon\CarbonImmutable $created_at
|
||||
* @property \Pterodactyl\Models\User|null $user
|
||||
* @property int $id
|
||||
* @property string $uuid
|
||||
* @property bool $is_system
|
||||
* @property int|null $user_id
|
||||
* @property int|null $server_id
|
||||
* @property string $action
|
||||
* @property string|null $subaction
|
||||
* @property array $device
|
||||
* @property array $metadata
|
||||
* @property \Carbon\CarbonImmutable $created_at
|
||||
* @property \Pterodactyl\Models\User|null $user
|
||||
* @property \Pterodactyl\Models\Server|null $server
|
||||
*/
|
||||
class AuditLog extends Model
|
||||
|
@ -36,6 +36,8 @@ class AuditLog extends Model
|
|||
public const SERVER__BACKUP_COMPELTED = 'server:backup.completed';
|
||||
public const SERVER__BACKUP_DELETED = 'server:backup.deleted';
|
||||
public const SERVER__BACKUP_DOWNLOADED = 'server:backup.downloaded';
|
||||
public const SERVER__BACKUP_LOCKED = 'server:backup.locked';
|
||||
public const SERVER__BACKUP_UNLOCKED = 'server:backup.unlocked';
|
||||
public const SERVER__BACKUP_RESTORE_STARTED = 'server:backup.restore.started';
|
||||
public const SERVER__BACKUP_RESTORE_COMPLETED = 'server:backup.restore.completed';
|
||||
public const SERVER__BACKUP_RESTORE_FAILED = 'server:backup.restore.failed';
|
||||
|
|
|
@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||
* @property int $server_id
|
||||
* @property string $uuid
|
||||
* @property bool $is_successful
|
||||
* @property bool $is_locked
|
||||
* @property string $name
|
||||
* @property string[] $ignored_files
|
||||
* @property string $disk
|
||||
|
@ -46,6 +47,7 @@ class Backup extends Model
|
|||
protected $casts = [
|
||||
'id' => 'int',
|
||||
'is_successful' => 'bool',
|
||||
'is_locked' => 'bool',
|
||||
'ignored_files' => 'array',
|
||||
'bytes' => 'int',
|
||||
];
|
||||
|
@ -62,6 +64,7 @@ class Backup extends Model
|
|||
*/
|
||||
protected $attributes = [
|
||||
'is_successful' => true,
|
||||
'is_locked' => false,
|
||||
'checksum' => null,
|
||||
'bytes' => 0,
|
||||
'upload_id' => null,
|
||||
|
@ -79,6 +82,7 @@ class Backup extends Model
|
|||
'server_id' => 'bail|required|numeric|exists:servers,id',
|
||||
'uuid' => 'required|uuid',
|
||||
'is_successful' => 'boolean',
|
||||
'is_locked' => 'boolean',
|
||||
'name' => 'required|string',
|
||||
'ignored_files' => 'array',
|
||||
'disk' => 'required|string',
|
||||
|
|
|
@ -18,6 +18,7 @@ use Pterodactyl\Contracts\Extensions\HashidsInterface;
|
|||
* @property string $cron_minute
|
||||
* @property bool $is_active
|
||||
* @property bool $is_processing
|
||||
* @property bool $only_when_online
|
||||
* @property \Carbon\Carbon|null $last_run_at
|
||||
* @property \Carbon\Carbon|null $next_run_at
|
||||
* @property \Carbon\Carbon $created_at
|
||||
|
@ -63,6 +64,7 @@ class Schedule extends Model
|
|||
'cron_minute',
|
||||
'is_active',
|
||||
'is_processing',
|
||||
'only_when_online',
|
||||
'last_run_at',
|
||||
'next_run_at',
|
||||
];
|
||||
|
@ -75,6 +77,7 @@ class Schedule extends Model
|
|||
'server_id' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'is_processing' => 'boolean',
|
||||
'only_when_online' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -99,6 +102,7 @@ class Schedule extends Model
|
|||
'cron_minute' => '*',
|
||||
'is_active' => true,
|
||||
'is_processing' => false,
|
||||
'only_when_online' => false,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -114,6 +118,7 @@ class Schedule extends Model
|
|||
'cron_minute' => 'required|string',
|
||||
'is_active' => 'boolean',
|
||||
'is_processing' => 'boolean',
|
||||
'only_when_online' => 'boolean',
|
||||
'last_run_at' => 'nullable|date',
|
||||
'next_run_at' => 'nullable|date',
|
||||
];
|
||||
|
@ -122,6 +127,7 @@ class Schedule extends Model
|
|||
* Returns the schedule's execution crontab entry as a string.
|
||||
*
|
||||
* @return \Carbon\CarbonImmutable
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getNextRunDate()
|
||||
|
|
|
@ -14,6 +14,7 @@ use Pterodactyl\Contracts\Extensions\HashidsInterface;
|
|||
* @property string $payload
|
||||
* @property int $time_offset
|
||||
* @property bool $is_queued
|
||||
* @property bool $continue_on_failure
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
* @property string $hashid
|
||||
|
@ -30,6 +31,13 @@ class Task extends Model
|
|||
*/
|
||||
public const RESOURCE_NAME = 'schedule_task';
|
||||
|
||||
/**
|
||||
* The default actions that can exist for a task in Pterodactyl.
|
||||
*/
|
||||
public const ACTION_POWER = 'power';
|
||||
public const ACTION_COMMAND = 'command';
|
||||
public const ACTION_BACKUP = 'backup';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
|
@ -56,6 +64,7 @@ class Task extends Model
|
|||
'payload',
|
||||
'time_offset',
|
||||
'is_queued',
|
||||
'continue_on_failure',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -69,6 +78,7 @@ class Task extends Model
|
|||
'sequence_id' => 'integer',
|
||||
'time_offset' => 'integer',
|
||||
'is_queued' => 'boolean',
|
||||
'continue_on_failure' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -79,6 +89,7 @@ class Task extends Model
|
|||
protected $attributes = [
|
||||
'time_offset' => 0,
|
||||
'is_queued' => false,
|
||||
'continue_on_failure' => false,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -91,6 +102,7 @@ class Task extends Model
|
|||
'payload' => 'required_unless:action,backup|string',
|
||||
'time_offset' => 'required|numeric|between:0,900',
|
||||
'is_queued' => 'boolean',
|
||||
'continue_on_failure' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,6 +9,7 @@ use Illuminate\Database\ConnectionInterface;
|
|||
use Pterodactyl\Extensions\Backups\BackupManager;
|
||||
use Pterodactyl\Repositories\Eloquent\BackupRepository;
|
||||
use Pterodactyl\Repositories\Wings\DaemonBackupRepository;
|
||||
use Pterodactyl\Exceptions\Service\Backup\BackupLockedException;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
class DeleteBackupService
|
||||
|
@ -55,6 +56,10 @@ class DeleteBackupService
|
|||
*/
|
||||
public function handle(Backup $backup)
|
||||
{
|
||||
if ($backup->is_locked) {
|
||||
throw new BackupLockedException();
|
||||
}
|
||||
|
||||
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
|
||||
$this->deleteFromS3($backup);
|
||||
|
||||
|
|
|
@ -21,6 +21,11 @@ class InitiateBackupService
|
|||
*/
|
||||
private $ignoredFiles;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isLocked = false;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\BackupRepository
|
||||
*/
|
||||
|
@ -49,7 +54,11 @@ class InitiateBackupService
|
|||
/**
|
||||
* InitiateBackupService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Eloquent\BackupRepository $repository
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonBackupRepository $daemonBackupRepository
|
||||
* @param \Pterodactyl\Services\Backups\DeleteBackupService $deleteBackupService
|
||||
* @param \Pterodactyl\Extensions\Backups\BackupManager $backupManager
|
||||
*/
|
||||
public function __construct(
|
||||
BackupRepository $repository,
|
||||
|
@ -65,6 +74,19 @@ class InitiateBackupService
|
|||
$this->deleteBackupService = $deleteBackupService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if the backup should be locked once it is created which will prevent
|
||||
* its deletion by users or automated system processes.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setIsLocked(bool $isLocked): self
|
||||
{
|
||||
$this->isLocked = $isLocked;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the files to be ignored by this backup.
|
||||
*
|
||||
|
@ -91,7 +113,7 @@ class InitiateBackupService
|
|||
}
|
||||
|
||||
/**
|
||||
* Initiates the backup process for a server on the daemon.
|
||||
* Initiates the backup process for a server on Wings.
|
||||
*
|
||||
* @throws \Throwable
|
||||
* @throws \Pterodactyl\Exceptions\Service\Backup\TooManyBackupsException
|
||||
|
@ -104,23 +126,30 @@ class InitiateBackupService
|
|||
if ($period > 0) {
|
||||
$previous = $this->repository->getBackupsGeneratedDuringTimespan($server->id, $period);
|
||||
if ($previous->count() >= $limit) {
|
||||
throw new TooManyRequestsHttpException(CarbonImmutable::now()->diffInSeconds($previous->last()->created_at->addSeconds($period)), sprintf('Only %d backups may be generated within a %d second span of time.', $limit, $period));
|
||||
$message = sprintf('Only %d backups may be generated within a %d second span of time.', $limit, $period);
|
||||
|
||||
throw new TooManyRequestsHttpException(CarbonImmutable::now()->diffInSeconds($previous->last()->created_at->addSeconds($period)), $message);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the server has reached or exceeded it's backup limit
|
||||
if (!$server->backup_limit || $server->backups()->where('is_successful', true)->count() >= $server->backup_limit) {
|
||||
// Check if the server has reached or exceeded it's backup limit.
|
||||
$successful = $server->backups()->where('is_successful', true);
|
||||
if (!$server->backup_limit || $successful->count() >= $server->backup_limit) {
|
||||
// Do not allow the user to continue if this server is already at its limit and can't override.
|
||||
if (!$override || $server->backup_limit <= 0) {
|
||||
throw new TooManyBackupsException($server->backup_limit);
|
||||
}
|
||||
|
||||
// Get the oldest backup the server has.
|
||||
/** @var \Pterodactyl\Models\Backup $oldestBackup */
|
||||
$oldestBackup = $server->backups()->where('is_successful', true)->orderBy('created_at')->first();
|
||||
// Get the oldest backup the server has that is not "locked" (indicating a backup that should
|
||||
// never be automatically purged). If we find a backup we will delete it and then continue with
|
||||
// this process. If no backup is found that can be used an exception is thrown.
|
||||
/** @var \Pterodactyl\Models\Backup $oldest */
|
||||
$oldest = $successful->where('is_locked', false)->orderBy('created_at')->first();
|
||||
if (!$oldest) {
|
||||
throw new TooManyBackupsException($server->backup_limit);
|
||||
}
|
||||
|
||||
// Delete the oldest backup.
|
||||
$this->deleteBackupService->handle($oldestBackup);
|
||||
$this->deleteBackupService->handle($oldest);
|
||||
}
|
||||
|
||||
return $this->connection->transaction(function () use ($server, $name) {
|
||||
|
@ -131,6 +160,7 @@ class InitiateBackupService
|
|||
'name' => trim($name) ?: sprintf('Backup at %s', CarbonImmutable::now()->toDateTimeString()),
|
||||
'ignored_files' => array_values($this->ignoredFiles ?? []),
|
||||
'disk' => $this->backupManager->getDefaultAdapter(),
|
||||
'is_locked' => $this->isLocked,
|
||||
], true, true);
|
||||
|
||||
$this->daemonBackupRepository->setServer($server)
|
||||
|
|
|
@ -83,7 +83,7 @@ class FindViableNodesService
|
|||
*
|
||||
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException
|
||||
*/
|
||||
public function handle(int $page = null)
|
||||
public function handle(int $perPage = null, int $page = null)
|
||||
{
|
||||
Assert::integer($this->disk, 'Disk space must be an int, got %s');
|
||||
Assert::integer($this->memory, 'Memory usage must be an int, got %s');
|
||||
|
@ -103,7 +103,7 @@ class FindViableNodesService
|
|||
->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [$this->disk]);
|
||||
|
||||
if (!is_null($page)) {
|
||||
$results = $results->paginate(50, ['*'], 'page', $page);
|
||||
$results = $results->paginate($perPage ?? 50, ['*'], 'page', $page);
|
||||
} else {
|
||||
$results = $results->get()->toBase();
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ use Illuminate\Contracts\Bus\Dispatcher;
|
|||
use Pterodactyl\Jobs\Schedule\RunTaskJob;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
class ProcessScheduleService
|
||||
{
|
||||
|
@ -21,13 +23,19 @@ class ProcessScheduleService
|
|||
*/
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Wings\DaemonServerRepository
|
||||
*/
|
||||
private $serverRepository;
|
||||
|
||||
/**
|
||||
* ProcessScheduleService constructor.
|
||||
*/
|
||||
public function __construct(ConnectionInterface $connection, Dispatcher $dispatcher)
|
||||
public function __construct(ConnectionInterface $connection, DaemonServerRepository $serverRepository, Dispatcher $dispatcher)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->connection = $connection;
|
||||
$this->serverRepository = $serverRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,7 +46,7 @@ class ProcessScheduleService
|
|||
public function handle(Schedule $schedule, bool $now = false)
|
||||
{
|
||||
/** @var \Pterodactyl\Models\Task $task */
|
||||
$task = $schedule->tasks()->orderBy('sequence_id', 'asc')->first();
|
||||
$task = $schedule->tasks()->orderBy('sequence_id')->first();
|
||||
|
||||
if (is_null($task)) {
|
||||
throw new DisplayException('Cannot process schedule for task execution: no tasks are registered.');
|
||||
|
@ -53,7 +61,31 @@ class ProcessScheduleService
|
|||
$task->update(['is_queued' => true]);
|
||||
});
|
||||
|
||||
$job = new RunTaskJob($task);
|
||||
$job = new RunTaskJob($task, $now);
|
||||
if ($schedule->only_when_online) {
|
||||
// Check that the server is currently in a starting or running state before executing
|
||||
// this schedule if this option has been set.
|
||||
try {
|
||||
$details = $this->serverRepository->setServer($schedule->server)->getDetails();
|
||||
$state = $details['state'] ?? 'offline';
|
||||
// If the server is stopping or offline just do nothing with this task.
|
||||
if (in_array($state, ['offline', 'stopping'])) {
|
||||
$job->failed();
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
if (!$exception instanceof DaemonConnectionException) {
|
||||
// If we encountered some exception during this process that wasn't just an
|
||||
// issue connecting to Wings run the failed sequence for a job. Otherwise we
|
||||
// can just quietly mark the task as completed without actually running anything.
|
||||
$job->failed($exception);
|
||||
}
|
||||
$job->failed();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$now) {
|
||||
$this->dispatcher->dispatch($job->delay($task->time_offset));
|
||||
|
|
|
@ -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\Traits\Commands;
|
||||
|
||||
|
@ -13,6 +6,20 @@ use Pterodactyl\Exceptions\PterodactylException;
|
|||
|
||||
trait EnvironmentWriterTrait
|
||||
{
|
||||
/**
|
||||
* Escapes an environment value by looking for any characters that could
|
||||
* reasonablly cause environment parsing issues. Those values are then wrapped
|
||||
* in quotes before being returned.
|
||||
*/
|
||||
public function escapeEnvironmentValue(string $value): string
|
||||
{
|
||||
if (!preg_match('/^\"(.*)\"$/', $value) && preg_match('/([^\w.\-+\/])+/', $value)) {
|
||||
return sprintf('"%s"', addslashes($value));
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the .env file for the application using the passed in values.
|
||||
*
|
||||
|
@ -28,14 +35,7 @@ trait EnvironmentWriterTrait
|
|||
$saveContents = file_get_contents($path);
|
||||
collect($values)->each(function ($value, $key) use (&$saveContents) {
|
||||
$key = strtoupper($key);
|
||||
// If the key value is not sorrounded by quotation marks, and contains anything that could reasonably
|
||||
// cause environment parsing issues, wrap it in quotes before writing it. This also adds slashes to the
|
||||
// value to ensure quotes within it don't cause us issues.
|
||||
if (!preg_match('/^\"(.*)\"$/', $value) && preg_match('/([^\w.\-+\/])+/', $value)) {
|
||||
$value = sprintf('"%s"', addslashes($value));
|
||||
}
|
||||
|
||||
$saveValue = sprintf('%s=%s', $key, $value);
|
||||
$saveValue = sprintf('%s=%s', $key, $this->escapeEnvironmentValue($value));
|
||||
|
||||
if (preg_match_all('/^' . $key . '=(.*)$/m', $saveContents) < 1) {
|
||||
$saveContents = $saveContents . PHP_EOL . $saveValue;
|
||||
|
|
|
@ -19,6 +19,7 @@ class BackupTransformer extends BaseClientTransformer
|
|||
return [
|
||||
'uuid' => $backup->uuid,
|
||||
'is_successful' => $backup->is_successful,
|
||||
'is_locked' => $backup->is_locked,
|
||||
'name' => $backup->name,
|
||||
'ignored_files' => $backup->ignored_files,
|
||||
'checksum' => $backup->checksum,
|
||||
|
|
|
@ -45,6 +45,7 @@ class ScheduleTransformer extends BaseClientTransformer
|
|||
],
|
||||
'is_active' => $model->is_active,
|
||||
'is_processing' => $model->is_processing,
|
||||
'only_when_online' => $model->only_when_online,
|
||||
'last_run_at' => $model->last_run_at ? $model->last_run_at->toIso8601String() : null,
|
||||
'next_run_at' => $model->next_run_at ? $model->next_run_at->toIso8601String() : null,
|
||||
'created_at' => $model->created_at->toIso8601String(),
|
||||
|
|
|
@ -21,13 +21,13 @@ class StatsTransformer extends BaseClientTransformer
|
|||
{
|
||||
return [
|
||||
'current_state' => Arr::get($data, 'state', 'stopped'),
|
||||
'is_suspended' => Arr::get($data, 'suspended', false),
|
||||
'is_suspended' => Arr::get($data, 'is_suspended', false),
|
||||
'resources' => [
|
||||
'memory_bytes' => Arr::get($data, 'memory_bytes', 0),
|
||||
'cpu_absolute' => Arr::get($data, 'cpu_absolute', 0),
|
||||
'disk_bytes' => Arr::get($data, 'disk_bytes', 0),
|
||||
'network_rx_bytes' => Arr::get($data, 'network.rx_bytes', 0),
|
||||
'network_tx_bytes' => Arr::get($data, 'network.tx_bytes', 0),
|
||||
'memory_bytes' => Arr::get($data, 'utilization.memory_bytes', 0),
|
||||
'cpu_absolute' => Arr::get($data, 'utilization.cpu_absolute', 0),
|
||||
'disk_bytes' => Arr::get($data, 'utilization.disk_bytes', 0),
|
||||
'network_rx_bytes' => Arr::get($data, 'utilization.network.rx_bytes', 0),
|
||||
'network_tx_bytes' => Arr::get($data, 'utilization.network.tx_bytes', 0),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ class TaskTransformer extends BaseClientTransformer
|
|||
'payload' => $model->payload,
|
||||
'time_offset' => $model->time_offset,
|
||||
'is_queued' => $model->is_queued,
|
||||
'continue_on_failure' => $model->continue_on_failure,
|
||||
'created_at' => $model->created_at->toIso8601String(),
|
||||
'updated_at' => $model->updated_at->toIso8601String(),
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue