diff --git a/app/Actions/Backup/ManageBackupFile.php b/app/Actions/Backup/ManageBackupFile.php index 9a522f741..1b844e2e9 100644 --- a/app/Actions/Backup/ManageBackupFile.php +++ b/app/Actions/Backup/ManageBackupFile.php @@ -8,33 +8,31 @@ use App\Http\Resources\BackupFileResource; use App\Jobs\Backup\DeleteFileJob; use App\Models\BackupFile; -use Illuminate\Support\Facades\Storage; -use Symfony\Component\HttpFoundation\StreamedResponse; -use Throwable; +use App\Models\Server; +use Illuminate\Support\Facades\Log; class ManageBackupFile { - /** - * @throws Throwable - */ - public function download(BackupFile $file): StreamedResponse + public function delete(BackupFile $file): void { - $file->backup->server->ssh()->download( - Storage::disk('tmp')->path(basename($file->path())), - $file->path() - ); + $server = Server::find($file->backup->server_id); - return Storage::disk('tmp')->download(basename($file->path())); - } + if ($server === null) { + Log::warning('Deleting orphaned backup file without a server', [ + 'backup_file_id' => $file->id, + 'backup_id' => $file->backup_id, + ]); + $file->delete(); + + return; + } - public function delete(BackupFile $file): void - { $file->status = BackupFileStatus::DELETING; $file->message = null; $file->save(); SocketEvent::dispatch(new SocketEventDTO( - projectId: $file->backup->server->project_id, + projectId: $server->project_id, type: 'backup-file.updated', data: new BackupFileResource($file), )); diff --git a/app/Console/Commands/RunBackupCommand.php b/app/Console/Commands/RunBackupCommand.php index 861851994..68ab4cbc8 100644 --- a/app/Console/Commands/RunBackupCommand.php +++ b/app/Console/Commands/RunBackupCommand.php @@ -20,6 +20,8 @@ public function handle(): void Backup::query() ->where('enabled', true) ->whereNull('status') + ->whereHas('server') + ->with('server') ->chunkById(100, function ($backups) use (&$total): void { /** @var Backup $backup */ foreach ($backups as $backup) { diff --git a/tests/Feature/BackupTest.php b/tests/Feature/BackupTest.php index bca5c619d..715dc57b2 100644 --- a/tests/Feature/BackupTest.php +++ b/tests/Feature/BackupTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature; +use App\Actions\Backup\ManageBackupFile; use App\Actions\Backup\RestoreBackup; use App\Actions\Backup\RunBackup; use App\Enums\BackupFileStatus; @@ -10,6 +11,7 @@ use App\Enums\DatabaseStatus; use App\Enums\ServerStatus; use App\Facades\SSH; +use App\Jobs\Backup\DeleteFileJob; use App\Jobs\Backup\RestoreDatabaseJob; use App\Models\Backup; use App\Models\BackupFile; @@ -603,6 +605,27 @@ public function test_cannot_enable_a_backup_being_deleted(): void ]); } + public function test_delete_orphaned_backup_file_hard_deletes_without_dispatching_job(): void + { + Bus::fake(); + + $backup = Backup::factory()->create([ + 'type' => BackupType::DATABASE, + 'server_id' => 999999, + 'storage_id' => $this->storageProvider->id, + 'status' => null, + ]); + $file = BackupFile::factory()->create([ + 'backup_id' => $backup->id, + 'status' => BackupFileStatus::CREATED, + ]); + + app(ManageBackupFile::class)->delete($file); + + $this->assertDatabaseMissing('backup_files', ['id' => $file->id]); + Bus::assertNotDispatched(DeleteFileJob::class); + } + public function test_see_global_backups_list_scoped_to_current_project(): void { $this->actingAs($this->user); diff --git a/tests/Unit/Commands/RunBackupCommandTest.php b/tests/Unit/Commands/RunBackupCommandTest.php index 37fa63e40..0d1da6fff 100644 --- a/tests/Unit/Commands/RunBackupCommandTest.php +++ b/tests/Unit/Commands/RunBackupCommandTest.php @@ -11,6 +11,7 @@ use App\Models\StorageProvider; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Http; use Tests\TestCase; @@ -121,4 +122,19 @@ public function test_runs_enabled_backup_even_after_a_failed_run(): void $this->artisan('backups:run') ->expectsOutput('1 backups started'); } + + public function test_does_not_run_backups_whose_server_is_missing(): void + { + SSH::fake(); + Bus::fake(); + Carbon::setTestNow('2026-06-19 10:00:00'); + + $backup = $this->createBackup(['interval' => '0 * * * *', 'server_id' => 999999]); + + $this->artisan('backups:run') + ->expectsOutput('0 backups started'); + + $this->assertDatabaseHas('backups', ['id' => $backup->id, 'server_id' => 999999]); + Bus::assertNothingDispatched(); + } }