feat(backups): support scheduled backups for application storage targets

Add polymorphic volume backup scheduling for persistent volumes and directories, expose schedule management via API, and reorganize backup configuration and execution views.
This commit is contained in:
Andras Bacsai committed 2026-07-15 17:34:22 +02:00
1 parent 995ec5fbb6
commit d7385ad0c4
60 files changed
+3570 -985

No files matched your search

@@ -2,11 +2,9 @@
namespace App\Livewire\Project\Database;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ServiceDatabase;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -18,31 +16,11 @@ class CreateScheduledBackup extends Component
#[Validate(['required', 'string'])]
public $frequency;
#[Validate(['required', 'boolean'])]
public bool $saveToS3 = false;
#[Locked]
public $database;
public bool $enabled = true;
#[Validate(['nullable', 'integer'])]
public ?int $s3StorageId = null;
public Collection $definedS3s;
public function mount()
{
try {
$this->definedS3s = currentTeam()->s3s;
if ($this->definedS3s->count() > 0) {
$this->s3StorageId = $this->definedS3s->first()->id;
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
@@ -50,20 +28,6 @@ class CreateScheduledBackup extends Component
$this->validate();
if ($this->saveToS3) {
$s3StorageExists = ! is_null($this->s3StorageId)
&& S3Storage::where('team_id', currentTeam()->id)
->where('is_usable', true)
->whereKey($this->s3StorageId)
->exists();
if (! $s3StorageExists) {
$this->dispatch('error', 'Please select a valid S3 storage to enable S3 backups.');
return;
}
}
$isValid = validate_cron_expression($this->frequency);
if (! $isValid) {
$this->dispatch('error', 'Invalid Cron / Human expression.');
@@ -74,8 +38,8 @@ class CreateScheduledBackup extends Component
$payload = [
'enabled' => true,
'frequency' => $this->frequency,
'save_s3' => $this->saveToS3,
's3_storage_id' => $this->s3StorageId,
'save_s3' => false,
's3_storage_id' => null,
'database_id' => $this->database->id,
'database_type' => $this->database->getMorphClass(),
'team_id' => currentTeam()->id,
@@ -91,11 +55,22 @@ class CreateScheduledBackup extends Component
$databaseBackup = ScheduledDatabaseBackup::create($payload);
if ($this->database->getMorphClass() === ServiceDatabase::class) {
$this->dispatch('refreshScheduledBackups', $databaseBackup->id);
$service = $this->database->service;
$this->redirectRoute('project.service.database.backup.show', [
'project_uuid' => $service->project()->uuid,
'environment_uuid' => $service->environment->uuid,
'service_uuid' => $service->uuid,
'stack_service_uuid' => $this->database->uuid,
'backup_uuid' => $databaseBackup->uuid,
], navigate: true);
} else {
$this->dispatch('refreshScheduledBackups');
$this->redirectRoute('project.database.backup.execution', [
'project_uuid' => $this->database->project()->uuid,
'environment_uuid' => $this->database->environment->uuid,
'database_uuid' => $this->database->uuid,
'backup_uuid' => $databaseBackup->uuid,
], navigate: true);
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {