Compare commits

..

6 Commits

Author SHA1 Message Date
Lance Pioch
0da15d483c Add option to delete S3 backups from storage on server deletion
When deleting a server, a checkbox now appears in the confirmation modal
if unlocked backups exist, allowing admins to also remove them from
storage via DeleteBackupService. Locked backups are always preserved.
2026-02-06 10:35:07 -05:00
Lance Pioch
b1a39f1724 Handle X-Forwarded-Proto in .htaccess for SSL-terminating proxies (#2171) 2026-02-06 09:54:24 -05:00
Lance Pioch
6a548c09a0 Clarify OAuth error when provider account has no linked email (#2179) 2026-02-06 07:50:37 -05:00
Lance Pioch
55bda569cc Implement flexible caching for node statuses (#2174) 2026-02-06 07:50:20 -05:00
Lance Pioch
adf1249086 Fix Egg Feature modals not working (#2175) 2026-02-06 07:49:56 -05:00
Lance Pioch
dbf77bf146 Implement single file move to support Unix mv semantics (#1984) (#2176) 2026-02-06 07:49:40 -05:00
14 changed files with 76 additions and 45 deletions

View File

@@ -30,6 +30,7 @@ use App\Traits\Filament\CanCustomizeTabs;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
@@ -1106,9 +1107,13 @@ class EditServer extends EditRecord
->modalHeading(trans('filament-actions::delete.single.modal.heading', ['label' => $this->getRecordTitle()]))
->modalSubmitActionLabel(trans('filament-actions::delete.single.label'))
->requiresConfirmation()
->action(function (Server $server, ServerDeletionService $service) {
->form(fn (Server $server) => $server->backups()->where('is_locked', false)->exists() ? [
Checkbox::make('delete_backups')
->label(trans('admin/server.delete_backups', ['count' => $server->backups()->where('is_locked', false)->count()])),
] : [])
->action(function (array $data, Server $server, ServerDeletionService $service) {
try {
$service->handle($server);
$service->withDeleteBackups($data['delete_backups'] ?? false)->handle($server);
return redirect(ListServers::getUrl(panel: 'admin'));
} catch (ConnectionException) {
@@ -1132,9 +1137,13 @@ class EditServer extends EditRecord
->modalHeading(trans('filament-actions::force-delete.single.modal.heading', ['label' => $this->getRecordTitle()]))
->modalSubmitActionLabel(trans('filament-actions::force-delete.single.label'))
->requiresConfirmation()
->action(function (Server $server, ServerDeletionService $service) {
->form(fn (Server $server) => $server->backups()->where('is_locked', false)->exists() ? [
Checkbox::make('delete_backups')
->label(trans('admin/server.delete_backups', ['count' => $server->backups()->where('is_locked', false)->count()])),
] : [])
->action(function (array $data, Server $server, ServerDeletionService $service) {
try {
$service->withForce()->handle($server);
$service->withForce()->withDeleteBackups($data['delete_backups'] ?? false)->handle($server);
return redirect(ListServers::getUrl(panel: 'admin'));
} catch (ConnectionException) {

View File

@@ -78,11 +78,15 @@ class Console extends Page
$feature = data_get($data, 'key');
$feature = $this->featureService->get($feature);
if (!$feature || $this->getMountedAction()) {
if (!$feature) {
return;
}
$this->mountAction($feature->getId());
sleep(2); // TODO find a better way
if ($this->getMountedAction()) {
$this->replaceMountedAction($feature->getId());
} else {
$this->mountAction($feature->getId());
}
}
public function getWidgetData(): array

View File

@@ -209,16 +209,18 @@ class ListFiles extends ListRecords
->required()
->live(),
TextEntry::make('new_location')
->state(fn (Get $get, File $file) => resolve_path(join_paths($this->path, $get('location') ?? '/', $file->name))),
->state(fn (Get $get, File $file) => resolve_path(join_paths($this->path, str_ends_with($get('location') ?? '/', '/') ? join_paths($get('location') ?? '/', $file->name) : $get('location') ?? '/'))),
])
->action(function ($data, File $file) {
$location = $data['location'];
$files = [['to' => join_paths($location, $file->name), 'from' => $file->name]];
$endsWithSlash = str_ends_with($location, '/');
$to = $endsWithSlash ? join_paths($location, $file->name) : $location;
$files = [['to' => $to, 'from' => $file->name]];
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
$oldLocation = join_paths($this->path, $file->name);
$newLocation = resolve_path(join_paths($this->path, $location, $file->name));
$newLocation = resolve_path(join_paths($this->path, $to));
Activity::event('server:file.rename')
->property('directory', $this->path)

View File

@@ -39,17 +39,13 @@ class CreateSchedule extends CreateRecord
$data['server_id'] = $server->id;
}
$timezone = $data['timezone'] ?? user()->timezone ?? 'UTC';
unset($data['timezone']);
if (!isset($data['next_run_at'])) {
$data['next_run_at'] = ScheduleResource::getNextRun(
$data['cron_minute'],
$data['cron_hour'],
$data['cron_day_of_month'],
$data['cron_month'],
$data['cron_day_of_week'],
$timezone
$data['cron_day_of_week']
);
}

View File

@@ -37,16 +37,12 @@ class EditSchedule extends EditRecord
protected function mutateFormDataBeforeSave(array $data): array
{
$timezone = $data['timezone'] ?? user()->timezone ?? 'UTC';
unset($data['timezone']);
$data['next_run_at'] = ScheduleResource::getNextRun(
$data['cron_minute'],
$data['cron_hour'],
$data['cron_day_of_month'],
$data['cron_month'],
$data['cron_day_of_week'],
$timezone
$data['cron_day_of_week']
);
return $data;

View File

@@ -100,15 +100,13 @@ class ScheduleResource extends Resource
Section::make('Cron')
->label(trans('server/schedule.cron'))
->description(function (Get $get) {
$timezone = $get('timezone') ?? user()->timezone ?? 'UTC';
try {
$nextRun = Utilities::getScheduleNextRunDate($get('cron_minute'), $get('cron_hour'), $get('cron_day_of_month'), $get('cron_month'), $get('cron_day_of_week'), $timezone)->timezone($timezone);
$nextRun = Utilities::getScheduleNextRunDate($get('cron_minute'), $get('cron_hour'), $get('cron_day_of_month'), $get('cron_month'), $get('cron_day_of_week'))->timezone(user()->timezone ?? 'UTC');
} catch (Exception) {
$nextRun = trans('server/schedule.invalid');
}
return new HtmlString(trans('server/schedule.cron_body', ['timezone' => $timezone]) . '<br>' . trans('server/schedule.cron_timezone', ['timezone' => $timezone, 'next_run' => $nextRun]));
return new HtmlString(trans('server/schedule.cron_body') . '<br>' . trans('server/schedule.cron_timezone', ['timezone' => user()->timezone ?? 'UTC', 'next_run' => $nextRun]));
})
->schema([
Actions::make([
@@ -297,13 +295,6 @@ class ScheduleResource extends Resource
'default' => 4,
'lg' => 5,
]),
Select::make('timezone')
->label(trans('server/schedule.timezone'))
->options(fn () => array_combine(timezone_identifiers_list(), timezone_identifiers_list()))
->default(user()->timezone ?? 'UTC')
->searchable()
->live()
->hiddenOn('view'),
])
->columnSpanFull(),
]);
@@ -388,10 +379,10 @@ class ScheduleResource extends Resource
];
}
public static function getNextRun(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek, string $timezone = 'UTC'): Carbon
public static function getNextRun(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon
{
try {
return Utilities::getScheduleNextRunDate($minute, $hour, $dayOfMonth, $month, $dayOfWeek, $timezone);
return Utilities::getScheduleNextRunDate($minute, $hour, $dayOfMonth, $month, $dayOfWeek);
} catch (Exception) {
Notification::make()
->title(trans('server/schedule.notification_invalid_cron'))

View File

@@ -38,11 +38,11 @@ class Utilities
*
* @throws Exception
*/
public static function getScheduleNextRunDate(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek, string $timezone = 'UTC'): Carbon
public static function getScheduleNextRunDate(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon
{
return Carbon::instance((new CronExpression(
sprintf('%s %s %s %s %s', $minute, $hour, $dayOfMonth, $month, $dayOfWeek)
))->getNextRunDate(now($timezone)))->setTimezone('UTC');
))->getNextRunDate(now('UTC')));
}
public static function checked(string $name, mixed $default): string

View File

@@ -75,7 +75,7 @@ class ScheduleController extends ClientApiController
'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, $request->user()->timezone ?? 'UTC'),
'next_run_at' => $this->getNextRunAt($request),
]);
Activity::event('server:schedule.create')
@@ -131,7 +131,7 @@ class ScheduleController extends ClientApiController
'cron_minute' => $request->input('minute'),
'is_active' => $active,
'only_when_online' => (bool) $request->input('only_when_online'),
'next_run_at' => $this->getNextRunAt($request, $request->user()->timezone ?? 'UTC'),
'next_run_at' => $this->getNextRunAt($request),
];
// Toggle the processing state of the scheduled task when it is enabled or disabled so that an
@@ -188,7 +188,7 @@ class ScheduleController extends ClientApiController
*
* @throws DisplayException
*/
protected function getNextRunAt(Request $request, string $timezone = 'UTC'): Carbon
protected function getNextRunAt(Request $request): Carbon
{
try {
return Utilities::getScheduleNextRunDate(
@@ -196,8 +196,7 @@ class ScheduleController extends ClientApiController
$request->input('hour'),
$request->input('day_of_month'),
$request->input('month'),
$request->input('day_of_week'),
$timezone
$request->input('day_of_week')
);
} catch (Exception) {
throw new DisplayException('The cron data provided does not evaluate to a valid expression.');

View File

@@ -74,7 +74,7 @@ class OAuthController extends Controller
$email = $oauthUser->getEmail();
if (!$email) {
return $this->errorRedirect();
return $this->errorRedirect('No email was linked to your account on the OAuth provider.');
}
$user = User::whereEmail($email)->first();

View File

@@ -358,7 +358,7 @@ class Node extends Model implements Validatable
'disk_used' => 0,
];
return cache()->remember("nodes.$this->id.statistics", now()->addSeconds(360), function () use ($default) {
return cache()->flexible("nodes.$this->id.statistics", [5, 30], function () use ($default) {
try {
$data = Http::daemon($this)

View File

@@ -5,6 +5,7 @@ namespace App\Services\Servers;
use App\Exceptions\DisplayException;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Backups\DeleteBackupService;
use App\Services\Databases\DatabaseManagementService;
use Exception;
use Illuminate\Database\ConnectionInterface;
@@ -16,13 +17,16 @@ class ServerDeletionService
{
protected bool $force = false;
protected bool $deleteBackups = false;
/**
* ServerDeletionService constructor.
*/
public function __construct(
private ConnectionInterface $connection,
private DaemonServerRepository $daemonServerRepository,
private DatabaseManagementService $databaseManagementService
private DatabaseManagementService $databaseManagementService,
private DeleteBackupService $deleteBackupService
) {}
/**
@@ -35,6 +39,16 @@ class ServerDeletionService
return $this;
}
/**
* Set if unlocked backups should be deleted from storage when the server is deleted.
*/
public function withDeleteBackups(bool $bool = true): self
{
$this->deleteBackups = $bool;
return $this;
}
/**
* Delete a server from the panel and remove any associated databases from hosts.
*
@@ -78,6 +92,22 @@ class ServerDeletionService
}
}
if ($this->deleteBackups) {
foreach ($server->backups()->where('is_locked', false)->get() as $backup) {
try {
$this->deleteBackupService->handle($backup);
} catch (Exception $exception) {
if (!$this->force) {
throw $exception;
}
$backup->delete();
logger()->warning($exception);
}
}
}
$server->allocations()->update([
'server_id' => null,
'notes' => null,

View File

@@ -98,6 +98,7 @@ return [
'delete_db' => 'Are you sure you want to delete :name ?',
'delete_db_heading' => 'Delete Database?',
'backups' => 'Backups',
'delete_backups' => 'Also delete :count backup(s) from storage',
'egg' => 'Egg',
'mounts' => 'Mounts',
'no_mounts' => 'No Mounts exist for this Node',

View File

@@ -29,8 +29,7 @@ return [
'enabled' => 'Enable Schedule?',
'enabled_hint' => 'This schedule will be executed automatically if enabled.',
'timezone' => 'Timezone',
'cron_body' => 'The cron inputs below use your timezone (:timezone).',
'cron_body' => 'Please keep in mind that the cron inputs below always assume UTC.',
'cron_timezone' => 'Next run in your timezone (:timezone): <b> :next_run </b>',
'invalid' => 'Invalid',

View File

@@ -5,6 +5,10 @@
RewriteEngine On
# Handle X-Forwarded-Proto Header
RewriteCond %{HTTP:X-Forwarded-Proto} =https [NC]
RewriteRule .* - [E=HTTPS:on]
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]