Refactor schedule tasks (#1911)

This commit is contained in:
Boy132 2025-11-24 14:42:47 +01:00 committed by GitHub
parent 611b8649e0
commit bb33bcca4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 342 additions and 79 deletions

View File

@ -0,0 +1,32 @@
<?php
namespace App\Extensions\Tasks\Schemas;
use App\Models\Schedule;
use App\Models\Task;
use App\Services\Backups\InitiateBackupService;
final class CreateBackupSchema extends TaskSchema
{
public function __construct(private InitiateBackupService $backupService) {}
public function getId(): string
{
return 'backup';
}
public function runTask(Task $task): void
{
$this->backupService->setIgnoredFiles(explode(PHP_EOL, $task->payload))->handle($task->server, null, true);
}
public function canCreate(Schedule $schedule): bool
{
return $schedule->server->backup_limit > 0;
}
public function getPayloadLabel(): string
{
return trans('server/schedule.tasks.actions.backup.files_to_ignore');
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Extensions\Tasks\Schemas;
use App\Models\Task;
use App\Services\Files\DeleteFilesService;
final class DeleteFilesSchema extends TaskSchema
{
public function __construct(private DeleteFilesService $deleteFilesService) {}
public function getId(): string
{
return 'delete_files';
}
public function runTask(Task $task): void
{
$this->deleteFilesService->handle($task->server, explode(PHP_EOL, $task->payload));
}
public function getPayloadLabel(): string
{
return trans('server/schedule.tasks.actions.delete_files.files_to_delete');
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Extensions\Tasks\Schemas;
use App\Models\Task;
use App\Repositories\Daemon\DaemonServerRepository;
use Filament\Forms\Components\Select;
use Filament\Schemas\Components\Component;
use Illuminate\Support\Str;
final class PowerActionSchema extends TaskSchema
{
public function __construct(private DaemonServerRepository $serverRepository) {}
public function getId(): string
{
return 'power';
}
public function runTask(Task $task): void
{
$this->serverRepository->setServer($task->server)->power($task->payload);
}
public function getDefaultPayload(): string
{
return 'restart';
}
public function getPayloadLabel(): string
{
return trans('server/schedule.tasks.actions.power.action');
}
public function formatPayload(string $payload): string
{
return Str::ucfirst($payload);
}
/** @return Component[] */
public function getPayloadForm(): array
{
return [
Select::make('payload')
->label($this->getPayloadLabel())
->required()
->options([
'start' => trans('server/schedule.tasks.actions.power.start'),
'restart' => trans('server/schedule.tasks.actions.power.restart'),
'stop' => trans('server/schedule.tasks.actions.power.stop'),
'kill' => trans('server/schedule.tasks.actions.power.kill'),
])
->selectablePlaceholder(false)
->default($this->getDefaultPayload()),
];
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Extensions\Tasks\Schemas;
use App\Models\Task;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Component;
final class SendCommandSchema extends TaskSchema
{
public function getId(): string
{
return 'command';
}
public function runTask(Task $task): void
{
$task->server->send($task->payload);
}
public function getPayloadLabel(): string
{
return trans('server/schedule.tasks.actions.command.command');
}
/** @return Component[] */
public function getPayloadForm(): array
{
return [
TextInput::make('payload')
->required()
->label($this->getPayloadLabel())
->default($this->getDefaultPayload()),
];
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Extensions\Tasks\Schemas;
use App\Extensions\Tasks\TaskSchemaInterface;
use App\Models\Schedule;
use Filament\Forms\Components\Textarea;
use Filament\Schemas\Components\Component;
abstract class TaskSchema implements TaskSchemaInterface
{
public function getName(): string
{
return trans('server/schedule.tasks.actions.' . $this->getId() . '.title');
}
public function canCreate(Schedule $schedule): bool
{
return true;
}
public function getDefaultPayload(): ?string
{
return null;
}
public function getPayloadLabel(): ?string
{
return null;
}
/** @return null|string|string[] */
public function formatPayload(string $payload): null|string|array
{
if (empty($payload)) {
return null;
}
return explode(PHP_EOL, $payload);
}
/** @return Component[] */
public function getPayloadForm(): array
{
return [
Textarea::make('payload')
->label($this->getPayloadLabel() ?? trans('server/schedule.tasks.payload'))
->default($this->getDefaultPayload())
->autosize(),
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Extensions\Tasks;
use App\Models\Schedule;
use App\Models\Task;
use Filament\Schemas\Components\Component;
interface TaskSchemaInterface
{
public function getId(): string;
public function getName(): string;
public function runTask(Task $task): void;
public function canCreate(Schedule $schedule): bool;
public function getDefaultPayload(): ?string;
public function getPayloadLabel(): ?string;
/** @return null|string|string[] */
public function formatPayload(string $payload): null|string|array;
/** @return Component[] */
public function getPayloadForm(): array;
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Extensions\Tasks;
class TaskService
{
/** @var array<string, TaskSchemaInterface> */
private array $schemas = [];
/**
* @return TaskSchemaInterface[]
*/
public function getAll(): array
{
return $this->schemas;
}
public function get(string $id): ?TaskSchemaInterface
{
return array_get($this->schemas, $id);
}
public function register(TaskSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
$this->schemas[$schema->getId()] = $schema;
}
/** @return array<string, string> */
public function getMappings(): array
{
return collect($this->schemas)->mapWithKeys(fn ($schema) => [$schema->getId() => $schema->getName()])->all();
}
}

View File

@ -2,6 +2,7 @@
namespace App\Filament\Server\Resources\Schedules\RelationManagers; namespace App\Filament\Server\Resources\Schedules\RelationManagers;
use App\Extensions\Tasks\TaskService;
use App\Facades\Activity; use App\Facades\Activity;
use App\Models\Schedule; use App\Models\Schedule;
use App\Models\Task; use App\Models\Task;
@ -9,12 +10,12 @@ use Exception;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Forms\Components\Field;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Components\Utilities\Set;
use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\IconColumn;
@ -26,50 +27,28 @@ class TasksRelationManager extends RelationManager
protected static string $relationship = 'tasks'; protected static string $relationship = 'tasks';
/** /**
* @return array<array-key, string> * @return Component[]
*/
private function getActionOptions(bool $full = true): array
{
return [
Task::ACTION_POWER => $full ? trans('server/schedule.tasks.actions.power.title') : trans('server/schedule.tasks.actions.power.action'),
Task::ACTION_COMMAND => $full ? trans('server/schedule.tasks.actions.command.title') : trans('server/schedule.tasks.actions.command.command'),
Task::ACTION_BACKUP => $full ? trans('server/schedule.tasks.actions.backup.title') : trans('server/schedule.tasks.actions.backup.files_to_ignore'),
Task::ACTION_DELETE_FILES => $full ? trans('server/schedule.tasks.actions.delete.title') : trans('server/schedule.tasks.actions.delete.files_to_delete'),
];
}
/**
* @return array<Field>
* *
* @throws Exception * @throws Exception
*/ */
private function getTaskForm(Schedule $schedule): array private function getTaskForm(Schedule $schedule): array
{ {
/** @var TaskService $taskService */
$taskService = app(TaskService::class); // @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
$tasks = $taskService->getAll();
return [ return [
Select::make('action') Select::make('action')
->label(trans('server/schedule.tasks.actions.title')) ->label(trans('server/schedule.tasks.actions.title'))
->required() ->required()
->live() ->live()
->disableOptionWhen(fn (string $value) => $value === Task::ACTION_BACKUP && $schedule->server->backup_limit === 0) ->disableOptionWhen(fn (string $value) => !$tasks[$value]->canCreate($schedule))
->options($this->getActionOptions()) ->options($taskService->getMappings())
->selectablePlaceholder(false) ->selectablePlaceholder(false)
->default(Task::ACTION_POWER) ->default(array_key_first($tasks))
->afterStateUpdated(fn ($state, Set $set) => $set('payload', $state === Task::ACTION_POWER ? 'restart' : null)), ->afterStateUpdated(fn ($state, Set $set) => $set('payload', $tasks[$state]->getDefaultPayload())),
Textarea::make('payload') Group::make(fn (Get $get) => array_key_exists($get('action'), $tasks) ? $tasks[$get('action')]->getPayloadForm() : []),
->hidden(fn (Get $get) => $get('action') === Task::ACTION_POWER)
->label(fn (Get $get) => $this->getActionOptions(false)[$get('action')] ?? trans('server/schedule.tasks.payload')),
Select::make('payload')
->visible(fn (Get $get) => $get('action') === Task::ACTION_POWER)
->label(trans('server/schedule.tasks.actions.power.action'))
->required()
->options([
'start' => trans('server/schedule.tasks.actions.power.start'),
'restart' => trans('server/schedule.tasks.actions.power.restart'),
'stop' => trans('server/schedule.tasks.actions.power.stop'),
'kill' => trans('server/schedule.tasks.actions.power.kill'),
])
->selectablePlaceholder(false)
->default('restart'),
TextInput::make('time_offset') TextInput::make('time_offset')
->label(trans('server/schedule.tasks.time_offset')) ->label(trans('server/schedule.tasks.time_offset'))
->hidden(fn (Get $get, ?Task $task) => config('queue.default') === 'sync' || $schedule->tasks->isEmpty() || $task?->isFirst()) ->hidden(fn (Get $get, ?Task $task) => config('queue.default') === 'sync' || $schedule->tasks->isEmpty() || $task?->isFirst())
@ -97,13 +76,12 @@ class TasksRelationManager extends RelationManager
->columns([ ->columns([
TextColumn::make('action') TextColumn::make('action')
->label(trans('server/schedule.tasks.actions.title')) ->label(trans('server/schedule.tasks.actions.title'))
->state(fn (Task $task) => $this->getActionOptions()[$task->action] ?? $task->action), ->state(fn (Task $task) => $task->getSchema()?->getName() ?? $task->action),
TextColumn::make('payload') TextColumn::make('payload')
->label(trans('server/schedule.tasks.payload')) ->label(trans('server/schedule.tasks.payload'))
->state(fn (Task $task) => match ($task->payload) { ->state(fn (Task $task) => $task->getSchema()?->formatPayload($task->payload) ?? $task->payload)
'start', 'restart', 'stop', 'kill' => mb_ucfirst($task->payload), ->tooltip(fn (Task $task) => $task->getSchema()?->getPayloadLabel())
default => explode(PHP_EOL, $task->payload) ->placeholder(trans('server/schedule.tasks.no_payload'))
})
->badge(), ->badge(),
TextColumn::make('time_offset') TextColumn::make('time_offset')
->label(trans('server/schedule.tasks.time_offset')) ->label(trans('server/schedule.tasks.time_offset'))

View File

@ -2,11 +2,9 @@
namespace App\Jobs\Schedule; namespace App\Jobs\Schedule;
use App\Extensions\Tasks\TaskService;
use App\Jobs\Job; use App\Jobs\Job;
use App\Models\Task; use App\Models\Task;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Backups\InitiateBackupService;
use App\Services\Files\DeleteFilesService;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Exception; use Exception;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@ -31,11 +29,8 @@ class RunTaskJob extends Job implements ShouldQueue
* *
* @throws Throwable * @throws Throwable
*/ */
public function handle( public function handle(TaskService $taskService): void
InitiateBackupService $backupService, {
DaemonServerRepository $serverRepository,
DeleteFilesService $deleteFilesService
): void {
// Do not process a task that is not set to active, unless it's been manually triggered. // 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) { if (!$this->task->schedule->is_active && !$this->manualRun) {
$this->markTaskNotQueued(); $this->markTaskNotQueued();
@ -57,22 +52,13 @@ class RunTaskJob extends Job implements ShouldQueue
// Perform the provided task against the daemon. // Perform the provided task against the daemon.
try { try {
switch ($this->task->action) { $taskSchema = $taskService->get($this->task->action);
case Task::ACTION_POWER:
$serverRepository->setServer($server)->power($this->task->payload); if (!$taskSchema) {
break; throw new InvalidArgumentException('Invalid task action provided: ' . $this->task->action);
case Task::ACTION_COMMAND:
$server->send($this->task->payload);
break;
case Task::ACTION_BACKUP:
$backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null, true);
break;
case Task::ACTION_DELETE_FILES:
$deleteFilesService->handle($server, explode(PHP_EOL, $this->task->payload));
break;
default:
throw new InvalidArgumentException('Invalid task action provided: ' . $this->task->action);
} }
$taskSchema->runTask($this->task);
} catch (Exception $exception) { } catch (Exception $exception) {
// If this isn't a ConnectionException on a task that allows for failures // If this isn't a ConnectionException on a task that allows for failures
// throw the exception back up the chain so that the task is stopped. // throw the exception back up the chain so that the task is stopped.

View File

@ -3,6 +3,8 @@
namespace App\Models; namespace App\Models;
use App\Contracts\Validatable; use App\Contracts\Validatable;
use App\Extensions\Tasks\TaskSchemaInterface;
use App\Extensions\Tasks\TaskService;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -35,17 +37,6 @@ class Task extends Model implements Validatable
*/ */
public const RESOURCE_NAME = 'schedule_task'; public const RESOURCE_NAME = 'schedule_task';
/**
* The default actions that can exist for a task
*/
public const ACTION_POWER = 'power';
public const ACTION_COMMAND = 'command';
public const ACTION_BACKUP = 'backup';
public const ACTION_DELETE_FILES = 'delete_files';
/** /**
* Relationships to be updated when this model is updated. * Relationships to be updated when this model is updated.
* *
@ -125,4 +116,12 @@ class Task extends Model implements Validatable
{ {
return $this->schedule->firstTask()?->id === $this->id; return $this->schedule->firstTask()?->id === $this->id;
} }
public function getSchema(): ?TaskSchemaInterface
{
/** @var TaskService $taskService */
$taskService = app(TaskService::class); // @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
return $taskService->get($this->action);
}
} }

View File

@ -0,0 +1,31 @@
<?php
namespace App\Providers\Extensions;
use App\Extensions\Tasks\Schemas\CreateBackupSchema;
use App\Extensions\Tasks\Schemas\DeleteFilesSchema;
use App\Extensions\Tasks\Schemas\PowerActionSchema;
use App\Extensions\Tasks\Schemas\SendCommandSchema;
use App\Extensions\Tasks\TaskService;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Backups\InitiateBackupService;
use App\Services\Files\DeleteFilesService;
use Illuminate\Support\ServiceProvider;
class TaskServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(TaskService::class, function ($app) {
$service = new TaskService();
// Default Task providers
$service->register(new PowerActionSchema($app->make(DaemonServerRepository::class)));
$service->register(new SendCommandSchema());
$service->register(new CreateBackupSchema($app->make(InitiateBackupService::class)));
$service->register(new DeleteFilesSchema($app->make(DeleteFilesService::class)));
return $service;
});
}
}

View File

@ -9,6 +9,7 @@ return [
App\Providers\Extensions\CaptchaServiceProvider::class, App\Providers\Extensions\CaptchaServiceProvider::class,
App\Providers\Extensions\FeatureServiceProvider::class, App\Providers\Extensions\FeatureServiceProvider::class,
App\Providers\Extensions\OAuthServiceProvider::class, App\Providers\Extensions\OAuthServiceProvider::class,
App\Providers\Extensions\TaskServiceProvider::class,
App\Providers\Filament\FilamentServiceProvider::class, App\Providers\Filament\FilamentServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class, App\Providers\Filament\AdminPanelProvider::class,
App\Providers\Filament\AppPanelProvider::class, App\Providers\Filament\AppPanelProvider::class,

View File

@ -76,6 +76,7 @@ return [
'limit' => 'Task Limit Reached', 'limit' => 'Task Limit Reached',
'action' => 'Action', 'action' => 'Action',
'payload' => 'Payload', 'payload' => 'Payload',
'no_payload' => 'No Payload',
'time_offset' => 'Time Offset', 'time_offset' => 'Time Offset',
'first_task' => 'First Task', 'first_task' => 'First Task',
'seconds' => 'Second|Seconds', 'seconds' => 'Second|Seconds',
@ -99,10 +100,9 @@ return [
'title' => 'Create Backup', 'title' => 'Create Backup',
'files_to_ignore' => 'Files to Ignore', 'files_to_ignore' => 'Files to Ignore',
], ],
'delete' => [ 'delete_files' => [
'title' => 'Delete Files', 'title' => 'Delete Files',
'files_to_delete' => 'Files to Delete', 'files_to_delete' => 'Files to Delete',
], ],
], ],
], ],

View File

@ -78,7 +78,7 @@ class RunTaskJobTest extends IntegrationTestCase
/** @var \App\Models\Task $task */ /** @var \App\Models\Task $task */
$task = Task::factory()->create([ $task = Task::factory()->create([
'schedule_id' => $schedule->id, 'schedule_id' => $schedule->id,
'action' => Task::ACTION_POWER, 'action' => 'power',
'payload' => 'start', 'payload' => 'start',
'is_queued' => true, 'is_queued' => true,
'continue_on_failure' => false, 'continue_on_failure' => false,
@ -112,7 +112,7 @@ class RunTaskJobTest extends IntegrationTestCase
/** @var \App\Models\Task $task */ /** @var \App\Models\Task $task */
$task = Task::factory()->create([ $task = Task::factory()->create([
'schedule_id' => $schedule->id, 'schedule_id' => $schedule->id,
'action' => Task::ACTION_POWER, 'action' => 'power',
'payload' => 'start', 'payload' => 'start',
'continue_on_failure' => $continueOnFailure, 'continue_on_failure' => $continueOnFailure,
]); ]);
@ -152,7 +152,7 @@ class RunTaskJobTest extends IntegrationTestCase
]); ]);
$task = Task::factory()->for($schedule)->create([ $task = Task::factory()->for($schedule)->create([
'action' => Task::ACTION_POWER, 'action' => 'power',
'payload' => 'start', 'payload' => 'start',
]); ]);