Add basic support for backups via the scheduled tasks system

This commit is contained in:
Dane Everitt 2020-04-19 19:43:41 -07:00
parent 7a3263f57b
commit 973591d86e
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
5 changed files with 54 additions and 36 deletions

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Http\Controllers\Api\Client\Servers; namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Carbon\Carbon;
use Pterodactyl\Models\Backup; use Pterodactyl\Models\Backup;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@ -11,7 +10,6 @@ use Pterodactyl\Repositories\Eloquent\BackupRepository;
use Pterodactyl\Services\Backups\InitiateBackupService; use Pterodactyl\Services\Backups\InitiateBackupService;
use Pterodactyl\Transformers\Api\Client\BackupTransformer; use Pterodactyl\Transformers\Api\Client\BackupTransformer;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\GetBackupsRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\GetBackupsRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\StoreBackupRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\StoreBackupRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\DeleteBackupRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\DeleteBackupRequest;
@ -78,14 +76,6 @@ class BackupController extends ClientApiController
*/ */
public function store(StoreBackupRequest $request, Server $server) public function store(StoreBackupRequest $request, Server $server)
{ {
$previous = $this->repository->getBackupsGeneratedDuringTimespan($server->id, 10);
if ($previous->count() >= 2) {
throw new TooManyRequestsHttpException(
Carbon::now()->diffInSeconds($previous->last()->created_at->addMinutes(10)),
'Only two backups may be generated within a 10 minute span of time.'
);
}
$backup = $this->initiateBackupService $backup = $this->initiateBackupService
->setIgnoredFiles( ->setIgnoredFiles(
explode(PHP_EOL, $request->input('ignored') ?? '') explode(PHP_EOL, $request->input('ignored') ?? '')

View file

@ -24,9 +24,9 @@ class StoreTaskRequest extends ViewScheduleRequest
public function rules(): array public function rules(): array
{ {
return [ return [
'action' => 'required|in:command,power', 'action' => 'required|in:command,power,backup',
'payload' => 'required|string', 'payload' => 'required_unless:action,backup|string',
'time_offset' => 'required|numeric|min:0|max:900', 'time_offset' => 'r=equired|numeric|min:0|max:900',
'sequence_id' => 'sometimes|required|numeric|min:1', 'sequence_id' => 'sometimes|required|numeric|min:1',
]; ];
} }

View file

@ -12,10 +12,10 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Pterodactyl\Repositories\Eloquent\TaskRepository; use Pterodactyl\Repositories\Eloquent\TaskRepository;
use Pterodactyl\Services\Backups\InitiateBackupService;
use Pterodactyl\Repositories\Wings\DaemonPowerRepository; use Pterodactyl\Repositories\Wings\DaemonPowerRepository;
use Pterodactyl\Repositories\Wings\DaemonCommandRepository; use Pterodactyl\Repositories\Wings\DaemonCommandRepository;
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
class RunTaskJob extends Job implements ShouldQueue class RunTaskJob extends Job implements ShouldQueue
@ -54,16 +54,16 @@ class RunTaskJob extends Job implements ShouldQueue
* Run the job and send actions to the daemon running the server. * Run the job and send actions to the daemon running the server.
* *
* @param \Pterodactyl\Repositories\Wings\DaemonCommandRepository $commandRepository * @param \Pterodactyl\Repositories\Wings\DaemonCommandRepository $commandRepository
* @param \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService $keyProviderService * @param \Pterodactyl\Services\Backups\InitiateBackupService $backupService
* @param \Pterodactyl\Repositories\Wings\DaemonPowerRepository $powerRepository * @param \Pterodactyl\Repositories\Wings\DaemonPowerRepository $powerRepository
* @param \Pterodactyl\Repositories\Eloquent\TaskRepository $taskRepository * @param \Pterodactyl\Repositories\Eloquent\TaskRepository $taskRepository
* *
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Throwable
*/ */
public function handle( public function handle(
DaemonCommandRepository $commandRepository, DaemonCommandRepository $commandRepository,
DaemonKeyProviderService $keyProviderService, InitiateBackupService $backupService,
DaemonPowerRepository $powerRepository, DaemonPowerRepository $powerRepository,
TaskRepository $taskRepository TaskRepository $taskRepository
) { ) {
@ -88,6 +88,9 @@ class RunTaskJob extends Job implements ShouldQueue
case 'command': case 'command':
$commandRepository->setServer($server)->send($task->payload); $commandRepository->setServer($server)->send($task->payload);
break; break;
case 'backup':
$backupService->setIgnoredFiles(explode(PHP_EOL, $task->payload))->handle($server, null);
break;
default: default:
throw new InvalidArgumentException('Cannot run a task that points to a non-existent action.'); throw new InvalidArgumentException('Cannot run a task that points to a non-existent action.');
} }

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Services\Backups; namespace Pterodactyl\Services\Backups;
use Carbon\Carbon;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
@ -10,6 +11,7 @@ use Pterodactyl\Models\Server;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Repositories\Eloquent\BackupRepository; use Pterodactyl\Repositories\Eloquent\BackupRepository;
use Pterodactyl\Repositories\Wings\DaemonBackupRepository; use Pterodactyl\Repositories\Wings\DaemonBackupRepository;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
class InitiateBackupService class InitiateBackupService
{ {
@ -85,6 +87,14 @@ class InitiateBackupService
*/ */
public function handle(Server $server, string $name = null): Backup public function handle(Server $server, string $name = null): Backup
{ {
$previous = $this->repository->getBackupsGeneratedDuringTimespan($server->id, 10);
if ($previous->count() >= 2) {
throw new TooManyRequestsHttpException(
Carbon::now()->diffInSeconds($previous->last()->created_at->addMinutes(10)),
'Only two backups may be generated within a 10 minute span of time.'
);
}
return $this->connection->transaction(function () use ($server, $name) { return $this->connection->transaction(function () use ($server, $name) {
/** @var \Pterodactyl\Models\Backup $backup */ /** @var \Pterodactyl\Models\Backup $backup */
$backup = $this->repository->create([ $backup = $this->repository->create([

View file

@ -36,14 +36,17 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
return ( return (
<Form className={'m-0'}> <Form className={'m-0'}>
<h3 className={'mb-6'}>Edit Task</h3> <h3 className={'mb-6'}>{isEditingTask ? 'Edit Task' : 'Create Task'}</h3>
<div className={'flex'}> <div className={'flex'}>
<div className={'mr-2'}> <div className={'mr-2 w-1/3'}>
<label className={'input-dark-label'}>Action</label> <label className={'input-dark-label'}>Action</label>
<FormikField as={'select'} name={'action'} className={'input-dark'}> <FormikFieldWrapper name={'action'}>
<option value={'command'}>Send command</option> <FormikField as={'select'} name={'action'} className={'input-dark'}>
<option value={'power'}>Send power action</option> <option value={'command'}>Send command</option>
</FormikField> <option value={'power'}>Send power action</option>
<option value={'backup'}>Create backup</option>
</FormikField>
</FormikFieldWrapper>
</div> </div>
<div className={'flex-1'}> <div className={'flex-1'}>
{action === 'command' ? {action === 'command' ?
@ -53,17 +56,25 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
description={'The command to send to the server when this task executes.'} description={'The command to send to the server when this task executes.'}
/> />
: :
<div> action === 'power' ?
<label className={'input-dark-label'}>Payload</label> <div>
<FormikFieldWrapper name={'payload'}> <label className={'input-dark-label'}>Payload</label>
<FormikField as={'select'} name={'payload'} className={'input-dark'}> <FormikFieldWrapper name={'payload'}>
<option value={'start'}>Start the server</option> <FormikField as={'select'} name={'payload'} className={'input-dark'}>
<option value={'restart'}>Restart the server</option> <option value={'start'}>Start the server</option>
<option value={'stop'}>Stop the server</option> <option value={'restart'}>Restart the server</option>
<option value={'kill'}>Terminate the server</option> <option value={'stop'}>Stop the server</option>
</FormikField> <option value={'kill'}>Terminate the server</option>
</FormikFieldWrapper> </FormikField>
</div> </FormikFieldWrapper>
</div>
:
<div>
<label className={'input-dark-label'}>Ignored Files</label>
<FormikFieldWrapper name={'payload'}>
<FormikField as={'textarea'} name={'payload'} className={'input-dark h-32'}/>
</FormikFieldWrapper>
</div>
} }
</div> </div>
</div> </div>
@ -120,8 +131,12 @@ export default ({ task, schedule, onDismissed }: Props) => {
timeOffset: task?.timeOffset.toString() || '0', timeOffset: task?.timeOffset.toString() || '0',
}} }}
validationSchema={object().shape({ validationSchema={object().shape({
action: string().required().oneOf([ 'command', 'power' ]), action: string().required().oneOf([ 'command', 'power', 'backup' ]),
payload: string().required('A task payload must be provided.'), payload: string().when('action', {
is: v => v !== 'backup',
then: string().required('A task payload must be provided.'),
otherwise: string(),
}),
timeOffset: number().typeError('The time offset must be a valid number between 0 and 900.') timeOffset: number().typeError('The time offset must be a valid number between 0 and 900.')
.required('A time offset value must be provided.') .required('A time offset value must be provided.')
.min(0, 'The time offset must be at least 0 seconds.') .min(0, 'The time offset must be at least 0 seconds.')