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

+10
View File
@@ -20,6 +20,7 @@ use App\Http\Controllers\Api\ServiceDatabasesController;
use App\Http\Controllers\Api\ServicesController;
use App\Http\Controllers\Api\TagsController;
use App\Http\Controllers\Api\TeamController;
use App\Http\Controllers\Api\VolumeBackupsController;
use App\Http\Controllers\Api\VultrController;
use App\Http\Middleware\ApiAllowed;
use Illuminate\Support\Facades\Route;
@@ -150,6 +151,9 @@ Route::group([
Route::post('/applications/{uuid}/storages', [ApplicationsController::class, 'create_storage'])->middleware(['api.ability:write']);
Route::patch('/applications/{uuid}/storages', [ApplicationsController::class, 'update_storage'])->middleware(['api.ability:write']);
Route::delete('/applications/{uuid}/storages/{storage_uuid}', [ApplicationsController::class, 'delete_storage'])->middleware(['api.ability:write']);
Route::put('/applications/{uuid}/storages/{storage_uuid}/backups', [VolumeBackupsController::class, 'upsert'])
->defaults('resource_type', 'application')
->middleware(['api.ability:write']);
Route::get('/applications/{uuid}/tags', [ApplicationsController::class, 'tags'])->middleware(['api.ability:read']);
Route::post('/applications/{uuid}/tags', [ApplicationsController::class, 'create_tag'])->middleware(['api.ability:write']);
@@ -194,6 +198,9 @@ Route::group([
Route::post('/databases/{uuid}/storages', [DatabasesController::class, 'create_storage'])->middleware(['api.ability:write']);
Route::patch('/databases/{uuid}/storages', [DatabasesController::class, 'update_storage'])->middleware(['api.ability:write']);
Route::delete('/databases/{uuid}/storages/{storage_uuid}', [DatabasesController::class, 'delete_storage'])->middleware(['api.ability:write']);
Route::put('/databases/{uuid}/storages/{storage_uuid}/backups', [VolumeBackupsController::class, 'upsert'])
->defaults('resource_type', 'database')
->middleware(['api.ability:write']);
Route::get('/databases/{uuid}/envs', [DatabasesController::class, 'envs'])->middleware(['api.ability:read']);
Route::post('/databases/{uuid}/envs', [DatabasesController::class, 'create_env'])->middleware(['api.ability:write']);
@@ -221,6 +228,9 @@ Route::group([
Route::post('/services/{uuid}/storages', [ServicesController::class, 'create_storage'])->middleware(['api.ability:write']);
Route::patch('/services/{uuid}/storages', [ServicesController::class, 'update_storage'])->middleware(['api.ability:write']);
Route::delete('/services/{uuid}/storages/{storage_uuid}', [ServicesController::class, 'delete_storage'])->middleware(['api.ability:write']);
Route::put('/services/{uuid}/storages/{storage_uuid}/backups', [VolumeBackupsController::class, 'upsert'])
->defaults('resource_type', 'service')
->middleware(['api.ability:write']);
Route::get('/services/{uuid}/envs', [ServicesController::class, 'envs'])->middleware(['api.ability:read']);
Route::post('/services/{uuid}/envs', [ServicesController::class, 'create_env'])->middleware(['api.ability:write']);
+14 -1
View File
@@ -229,6 +229,10 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/persistent-storage', ApplicationConfiguration::class)->name('project.application.persistent-storage');
Route::get('/backups', ApplicationBackupIndex::class)->name('project.application.backup.index');
Route::get('/backups/{backup_uuid}', ApplicationBackupShow::class)->name('project.application.backup.show');
Route::get('/backups/{backup_uuid}/s3', ApplicationBackupShow::class)->name('project.application.backup.s3');
Route::get('/backups/{backup_uuid}/retention', ApplicationBackupShow::class)->name('project.application.backup.retention');
Route::get('/backups/{backup_uuid}/executions', ApplicationBackupShow::class)->name('project.application.backup.executions');
Route::get('/backups/{backup_uuid}/danger', ApplicationBackupShow::class)->name('project.application.backup.danger');
Route::get('/source', ApplicationConfiguration::class)->name('project.application.source');
Route::get('/servers', ApplicationConfiguration::class)->name('project.application.servers');
Route::get('/scheduled-tasks', ApplicationConfiguration::class)->name('project.application.scheduled-tasks.show');
@@ -266,6 +270,10 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/terminal', ExecuteContainerCommand::class)->name('project.database.command')->middleware('can.access.terminal');
Route::get('/backups', DatabaseBackupIndex::class)->name('project.database.backup.index');
Route::get('/backups/{backup_uuid}', DatabaseBackupExecution::class)->name('project.database.backup.execution');
Route::get('/backups/{backup_uuid}/s3', DatabaseBackupExecution::class)->name('project.database.backup.s3');
Route::get('/backups/{backup_uuid}/retention', DatabaseBackupExecution::class)->name('project.database.backup.retention');
Route::get('/backups/{backup_uuid}/executions', DatabaseBackupExecution::class)->name('project.database.backup.executions');
Route::get('/backups/{backup_uuid}/danger', DatabaseBackupExecution::class)->name('project.database.backup.danger');
});
Route::prefix('project/{project_uuid}/environment/{environment_uuid}/service/{service_uuid}')->group(function () {
Route::get('/', ServiceConfiguration::class)->name('project.service.configuration');
@@ -279,6 +287,11 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/danger', ServiceConfiguration::class)->name('project.service.danger');
Route::get('/terminal', ExecuteContainerCommand::class)->name('project.service.command')->middleware('can.access.terminal');
Route::get('/{stack_service_uuid}/backups', ServiceDatabaseBackups::class)->name('project.service.database.backups');
Route::get('/{stack_service_uuid}/backups/{backup_uuid}', ServiceDatabaseBackups::class)->name('project.service.database.backup.show');
Route::get('/{stack_service_uuid}/backups/{backup_uuid}/s3', ServiceDatabaseBackups::class)->name('project.service.database.backup.s3');
Route::get('/{stack_service_uuid}/backups/{backup_uuid}/retention', ServiceDatabaseBackups::class)->name('project.service.database.backup.retention');
Route::get('/{stack_service_uuid}/backups/{backup_uuid}/executions', ServiceDatabaseBackups::class)->name('project.service.database.backup.executions');
Route::get('/{stack_service_uuid}/backups/{backup_uuid}/danger', ServiceDatabaseBackups::class)->name('project.service.database.backup.danger');
Route::get('/{stack_service_uuid}/import', ServiceIndex::class)->name('project.service.database.import')->middleware('can.update.resource');
Route::get('/{stack_service_uuid}/advanced', ServiceIndex::class)->name('project.service.index.advanced');
Route::get('/{stack_service_uuid}', ServiceIndex::class)->name('project.service.index');
@@ -428,7 +441,7 @@ Route::middleware(['auth'])->group(function () {
}
$execution = ScheduledVolumeBackupExecution::query()
->with('scheduledVolumeBackup.volume.resource')
->with('scheduledVolumeBackup.backupable.resource')
->findOrFail(request()->route('executionId'));
if ($team->id !== 0 && $team->id !== $execution->scheduledVolumeBackup->team_id) {
return response()->json(['message' => 'Permission denied.'], 403);