mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-22 22:23:48 +00:00
fix(backups): honor full dumps and selected storage deletion (#11331)
This commit is contained in:
@@ -279,33 +279,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (str($databaseType)->contains('postgres')) {
|
||||
// Format: db1,db2,db3
|
||||
$databasesToBackup = explode(',', $databasesToBackup);
|
||||
$databasesToBackup = array_map('trim', $databasesToBackup);
|
||||
} elseif (str($databaseType)->contains('mongo')) {
|
||||
// Format: db1:collection1,collection2|db2:collection3,collection4
|
||||
// Only explode if it's a string, not if it's already an array
|
||||
if (is_string($databasesToBackup)) {
|
||||
$databasesToBackup = explode('|', $databasesToBackup);
|
||||
$databasesToBackup = array_map('trim', $databasesToBackup);
|
||||
}
|
||||
} elseif (str($databaseType)->contains('mysql')) {
|
||||
// Format: db1,db2,db3
|
||||
$databasesToBackup = explode(',', $databasesToBackup);
|
||||
$databasesToBackup = array_map('trim', $databasesToBackup);
|
||||
} elseif (str($databaseType)->contains('mariadb')) {
|
||||
// Format: db1,db2,db3
|
||||
$databasesToBackup = explode(',', $databasesToBackup);
|
||||
$databasesToBackup = array_map('trim', $databasesToBackup);
|
||||
} elseif ($this->database instanceof StandaloneClickhouse) {
|
||||
// Format: db1,db2,db3
|
||||
$databasesToBackup = explode(',', $databasesToBackup);
|
||||
$databasesToBackup = array_map('trim', $databasesToBackup);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$databasesToBackup = $this->databasesToBackup($databaseType, $databasesToBackup);
|
||||
if ($databasesToBackup === []) {
|
||||
return;
|
||||
}
|
||||
$this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name;
|
||||
if ($this->database->name === 'coolify-db') {
|
||||
@@ -600,6 +577,30 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<int, string> */
|
||||
private function databasesToBackup(string $databaseType, string|array $databases): array
|
||||
{
|
||||
$type = str($databaseType);
|
||||
|
||||
if ($this->backup->dump_all && $type->contains(['postgres', 'mysql', 'mariadb'])) {
|
||||
return ['all'];
|
||||
}
|
||||
|
||||
if (is_array($databases)) {
|
||||
return $databases;
|
||||
}
|
||||
|
||||
if ($type->contains('mongo')) {
|
||||
return array_map('trim', explode('|', $databases));
|
||||
}
|
||||
|
||||
if ($type->contains(['postgres', 'mysql', 'mariadb', 'clickhouse'])) {
|
||||
return array_map('trim', explode(',', $databases));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function backup_standalone_postgresql(string $database): void
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -98,26 +98,34 @@ class BackupExecutions extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$server = $execution->scheduledDatabaseBackup->database->getMorphClass() === ServiceDatabase::class
|
||||
? $execution->scheduledDatabaseBackup->database->service->destination->server
|
||||
: $execution->scheduledDatabaseBackup->database->destination->server;
|
||||
|
||||
try {
|
||||
if ($execution->filename) {
|
||||
deleteBackupsLocally($execution->filename, $server);
|
||||
$deleteFromS3 = in_array('delete_backup_s3', $selectedActions, true);
|
||||
|
||||
if ($this->delete_backup_s3 && $execution->scheduledDatabaseBackup->s3) {
|
||||
deleteBackupsS3($execution->filename, $execution->scheduledDatabaseBackup->s3);
|
||||
if ($execution->filename && ! $execution->local_storage_deleted) {
|
||||
$server = $this->backup->server();
|
||||
if (! $server) {
|
||||
throw new \RuntimeException('The backup server is unavailable.');
|
||||
}
|
||||
|
||||
deleteBackupsLocally($execution->filename, $server, throwError: true);
|
||||
}
|
||||
|
||||
if ($deleteFromS3 && $execution->s3_uploaded && ! $execution->s3_storage_deleted) {
|
||||
if (! $execution->scheduledDatabaseBackup->s3) {
|
||||
throw new \RuntimeException('The S3 storage is unavailable.');
|
||||
}
|
||||
|
||||
deleteBackupsS3($execution->filename, $execution->scheduledDatabaseBackup->s3);
|
||||
}
|
||||
|
||||
$execution->delete();
|
||||
$this->delete_backup_s3 = false;
|
||||
$this->dispatch('success', 'Backup deleted.');
|
||||
$this->refreshBackupExecutions();
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', 'Failed to delete backup: '.$e->getMessage());
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -11,6 +11,7 @@ class ScheduledDatabaseBackup extends BaseModel
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'dump_all' => 'boolean',
|
||||
'database_backup_retention_max_storage_locally' => 'float',
|
||||
'database_backup_retention_max_storage_s3' => 'float',
|
||||
];
|
||||
|
||||
@@ -44,6 +44,14 @@ test('scheduled database backup execution model casts storage deletion fields co
|
||||
expect($casts['s3_storage_deleted'])->toBe('boolean');
|
||||
});
|
||||
|
||||
test('scheduled database backup casts full dump selection to boolean', function () {
|
||||
$model = new ScheduledDatabaseBackup;
|
||||
|
||||
expect($model->getCasts())->toMatchArray(['dump_all' => 'boolean'])
|
||||
->and((new ScheduledDatabaseBackup(['dump_all' => '0']))->dump_all)->toBeFalse()
|
||||
->and((new ScheduledDatabaseBackup(['dump_all' => '1']))->dump_all)->toBeTrue();
|
||||
});
|
||||
|
||||
test('upload_to_s3 throws exception and disables s3 when storage is null', function () {
|
||||
$backup = ScheduledDatabaseBackup::create([
|
||||
'frequency' => '0 0 * * *',
|
||||
@@ -362,3 +370,41 @@ test('all dump all database commands use shared helper compression', function ()
|
||||
->and(substr_count($source, '$this->buildCompressedDumpCommand($dumpCommand)'))->toBe(2)
|
||||
->and($source)->not->toContain('| gzip >');
|
||||
});
|
||||
|
||||
test('full database dumps create one logical all-databases archive regardless of saved database names', function (string $databaseType) {
|
||||
$backup = new ScheduledDatabaseBackup([
|
||||
'dump_all' => true,
|
||||
'databases_to_backup' => 'default,analytics',
|
||||
]);
|
||||
$job = new DatabaseBackupJob($backup);
|
||||
|
||||
$databases = (new ReflectionClass($job))
|
||||
->getMethod('databasesToBackup')
|
||||
->invoke($job, $databaseType, $backup->databases_to_backup);
|
||||
|
||||
expect($databases)->toBe(['all']);
|
||||
})->with(['postgresql', 'mysql', 'mariadb']);
|
||||
|
||||
test('specific database dumps keep every selected database', function (string $databaseType) {
|
||||
$backup = new ScheduledDatabaseBackup([
|
||||
'dump_all' => false,
|
||||
'databases_to_backup' => 'default, analytics',
|
||||
]);
|
||||
$job = new DatabaseBackupJob($backup);
|
||||
|
||||
$databases = (new ReflectionClass($job))
|
||||
->getMethod('databasesToBackup')
|
||||
->invoke($job, $databaseType, $backup->databases_to_backup);
|
||||
|
||||
expect($databases)->toBe(['default', 'analytics']);
|
||||
})->with(['postgresql', 'mysql', 'mariadb']);
|
||||
|
||||
test('individual database backup deletion surfaces local failures and honors selected S3 deletion', function () {
|
||||
$source = file_get_contents(app_path('Livewire/Project/Database/BackupExecutions.php'));
|
||||
|
||||
expect($source)
|
||||
->toContain("in_array('delete_backup_s3', \$selectedActions, true)")
|
||||
->toContain('deleteBackupsLocally($execution->filename, $server, throwError: true)')
|
||||
->toContain("throw new \\RuntimeException('The backup server is unavailable.')")
|
||||
->not->toContain('deleteBackupsLocally($execution->filename, $server);');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user