mirror of
https://github.com/coollabsio/coolify.git
synced 2026-03-11 08:55:47 +00:00
Merge 1095f248bf into 5a2547c879
This commit is contained in:
commit
13e8ef02b5
22 changed files with 259 additions and 596 deletions
|
|
@ -54,7 +54,6 @@ class Emails extends Command
|
|||
options: [
|
||||
'updates' => 'Send Update Email to all users',
|
||||
'emails-test' => 'Test',
|
||||
'database-backup-statuses-daily' => 'Database - Backup Statuses (Daily)',
|
||||
'application-deployment-success-daily' => 'Application - Deployment Success (Daily)',
|
||||
'application-deployment-success' => 'Application - Deployment Success',
|
||||
'application-deployment-failed' => 'Application - Deployment Failed',
|
||||
|
|
@ -167,7 +166,7 @@ class Emails extends Command
|
|||
]);
|
||||
}
|
||||
$output = 'Because of an error, the backup of the database '.$db->name.' failed.';
|
||||
$this->mail = (new BackupFailed($backup, $db, $output, $backup->database_name ?? 'unknown'))->toMail();
|
||||
$this->mail = (new BackupFailed($backup, $db, $output))->toMail();
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'backup-success':
|
||||
|
|
|
|||
|
|
@ -627,8 +627,6 @@ class DatabasesController extends Controller
|
|||
'enabled' => ['type' => 'boolean', 'description' => 'Whether the backup is enabled', 'default' => true],
|
||||
'save_s3' => ['type' => 'boolean', 'description' => 'Whether to save backups to S3', 'default' => false],
|
||||
's3_storage_uuid' => ['type' => 'string', 'description' => 'S3 storage UUID (required if save_s3 is true)'],
|
||||
'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'],
|
||||
'dump_all' => ['type' => 'boolean', 'description' => 'Whether to dump all databases', 'default' => false],
|
||||
'backup_now' => ['type' => 'boolean', 'description' => 'Whether to trigger backup immediately after creation'],
|
||||
'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Number of backups to retain locally'],
|
||||
'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Number of days to retain backups locally'],
|
||||
|
|
@ -672,7 +670,7 @@ class DatabasesController extends Controller
|
|||
)]
|
||||
public function create_backup(Request $request)
|
||||
{
|
||||
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid'];
|
||||
$backupConfigFields = ['save_s3', 'enabled', 'frequency', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
|
|
@ -689,10 +687,8 @@ class DatabasesController extends Controller
|
|||
'frequency' => 'required|string',
|
||||
'enabled' => 'boolean',
|
||||
'save_s3' => 'boolean',
|
||||
'dump_all' => 'boolean',
|
||||
'backup_now' => 'boolean|nullable',
|
||||
's3_storage_uuid' => 'string|exists:s3_storages,uuid|nullable',
|
||||
'databases_to_backup' => 'string|nullable',
|
||||
'database_backup_retention_amount_locally' => 'integer|min:0',
|
||||
'database_backup_retention_days_locally' => 'integer|min:0',
|
||||
'database_backup_retention_max_storage_locally' => 'integer|min:0',
|
||||
|
|
@ -777,17 +773,6 @@ class DatabasesController extends Controller
|
|||
unset($backupData['s3_storage_uuid']);
|
||||
}
|
||||
|
||||
// Set default databases_to_backup based on database type if not provided
|
||||
if (! isset($backupData['databases_to_backup']) || empty($backupData['databases_to_backup'])) {
|
||||
if ($database->type() === 'standalone-postgresql') {
|
||||
$backupData['databases_to_backup'] = $database->postgres_db;
|
||||
} elseif ($database->type() === 'standalone-mysql') {
|
||||
$backupData['databases_to_backup'] = $database->mysql_database;
|
||||
} elseif ($database->type() === 'standalone-mariadb') {
|
||||
$backupData['databases_to_backup'] = $database->mariadb_database;
|
||||
}
|
||||
}
|
||||
|
||||
// Add required fields
|
||||
$backupData['database_id'] = $database->id;
|
||||
$backupData['database_type'] = $database->getMorphClass();
|
||||
|
|
@ -852,8 +837,6 @@ class DatabasesController extends Controller
|
|||
's3_storage_uuid' => ['type' => 'string', 'description' => 'S3 storage UUID'],
|
||||
'backup_now' => ['type' => 'boolean', 'description' => 'Whether to take a backup now or not'],
|
||||
'enabled' => ['type' => 'boolean', 'description' => 'Whether the backup is enabled or not'],
|
||||
'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'],
|
||||
'dump_all' => ['type' => 'boolean', 'description' => 'Whether all databases are dumped or not'],
|
||||
'frequency' => ['type' => 'string', 'description' => 'Frequency of the backup'],
|
||||
'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Retention amount of the backup locally'],
|
||||
'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Retention days of the backup locally'],
|
||||
|
|
@ -890,7 +873,7 @@ class DatabasesController extends Controller
|
|||
)]
|
||||
public function update_backup(Request $request)
|
||||
{
|
||||
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid'];
|
||||
$backupConfigFields = ['save_s3', 'enabled', 'frequency', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
|
|
@ -905,9 +888,7 @@ class DatabasesController extends Controller
|
|||
'save_s3' => 'boolean',
|
||||
'backup_now' => 'boolean|nullable',
|
||||
'enabled' => 'boolean',
|
||||
'dump_all' => 'boolean',
|
||||
's3_storage_uuid' => 'string|exists:s3_storages,uuid|nullable',
|
||||
'databases_to_backup' => 'string|nullable',
|
||||
'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly',
|
||||
'database_backup_retention_amount_locally' => 'integer|min:0',
|
||||
'database_backup_retention_days_locally' => 'integer|min:0',
|
||||
|
|
|
|||
|
|
@ -83,8 +83,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$databasesToBackup = null;
|
||||
|
||||
$this->team = Team::find($this->backup->team_id);
|
||||
if (! $this->team) {
|
||||
$this->backup->delete();
|
||||
|
|
@ -133,15 +131,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
$this->database->postgres_user = 'postgres';
|
||||
}
|
||||
|
||||
$db = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('POSTGRES_DB=');
|
||||
})->first();
|
||||
|
||||
if ($db) {
|
||||
$databasesToBackup = str($db)->after('POSTGRES_DB=')->value();
|
||||
} else {
|
||||
$databasesToBackup = $this->database->postgres_user;
|
||||
}
|
||||
$this->postgres_password = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('POSTGRES_PASSWORD=');
|
||||
})->first();
|
||||
|
|
@ -161,16 +150,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
if ($rootPassword) {
|
||||
$this->database->mysql_root_password = str($rootPassword)->after('MYSQL_ROOT_PASSWORD=')->value();
|
||||
}
|
||||
|
||||
$db = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MYSQL_DATABASE=');
|
||||
})->first();
|
||||
|
||||
if ($db) {
|
||||
$databasesToBackup = str($db)->after('MYSQL_DATABASE=')->value();
|
||||
} else {
|
||||
throw new \Exception('MYSQL_DATABASE not found');
|
||||
}
|
||||
} elseif (str($databaseType)->contains('mariadb')) {
|
||||
$this->container_name = "{$this->database->name}-$serviceUuid";
|
||||
$this->directory_name = $serviceName.'-'.$this->container_name;
|
||||
|
|
@ -190,26 +169,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
$this->database->mariadb_root_password = str($rootPassword)->after('MYSQL_ROOT_PASSWORD=')->value();
|
||||
}
|
||||
}
|
||||
|
||||
$db = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MARIADB_DATABASE=');
|
||||
})->first();
|
||||
|
||||
if ($db) {
|
||||
$databasesToBackup = str($db)->after('MARIADB_DATABASE=')->value();
|
||||
} else {
|
||||
$db = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MYSQL_DATABASE=');
|
||||
})->first();
|
||||
|
||||
if ($db) {
|
||||
$databasesToBackup = str($db)->after('MYSQL_DATABASE=')->value();
|
||||
} else {
|
||||
throw new \Exception('MARIADB_DATABASE or MYSQL_DATABASE not found');
|
||||
}
|
||||
}
|
||||
} elseif (str($databaseType)->contains('mongo')) {
|
||||
$databasesToBackup = ['*'];
|
||||
$this->container_name = "{$this->database->name}-$serviceUuid";
|
||||
$this->directory_name = $serviceName.'-'.$this->container_name;
|
||||
|
||||
|
|
@ -244,44 +204,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
$this->container_name = $this->database->uuid;
|
||||
$this->directory_name = $databaseName.'-'.$this->container_name;
|
||||
$databaseType = $this->database->type();
|
||||
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
|
||||
}
|
||||
if (blank($databasesToBackup)) {
|
||||
if (str($databaseType)->contains('postgres')) {
|
||||
$databasesToBackup = [$this->database->postgres_db];
|
||||
} elseif (str($databaseType)->contains('mongo')) {
|
||||
$databasesToBackup = ['*'];
|
||||
} elseif (str($databaseType)->contains('mysql')) {
|
||||
$databasesToBackup = [$this->database->mysql_database];
|
||||
} elseif (str($databaseType)->contains('mariadb')) {
|
||||
$databasesToBackup = [$this->database->mariadb_database];
|
||||
} 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);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name;
|
||||
if ($this->database->name === 'coolify-db') {
|
||||
$databasesToBackup = ['coolify'];
|
||||
|
|
@ -289,156 +213,131 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
$ip = Str::slug($this->server->ip);
|
||||
$this->backup_dir = backup_dir().'/coolify'."/coolify-db-$ip";
|
||||
}
|
||||
foreach ($databasesToBackup as $database) {
|
||||
// Generate unique UUID for each database backup execution
|
||||
$attempts = 0;
|
||||
do {
|
||||
$this->backup_log_uuid = (string) new Cuid2;
|
||||
$exists = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->exists();
|
||||
$attempts++;
|
||||
if ($attempts >= 3 && $exists) {
|
||||
throw new \Exception('Unable to generate unique UUID for backup execution after 3 attempts');
|
||||
}
|
||||
} while ($exists);
|
||||
|
||||
$size = 0;
|
||||
$localBackupSucceeded = false;
|
||||
$s3UploadError = null;
|
||||
// Generate unique UUID for backup execution
|
||||
$attempts = 0;
|
||||
do {
|
||||
$this->backup_log_uuid = (string) new Cuid2;
|
||||
$exists = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->exists();
|
||||
$attempts++;
|
||||
if ($attempts >= 3 && $exists) {
|
||||
throw new \Exception('Unable to generate unique UUID for backup execution after 3 attempts');
|
||||
}
|
||||
} while ($exists);
|
||||
|
||||
// Step 1: Create local backup
|
||||
$size = 0;
|
||||
$localBackupSucceeded = false;
|
||||
$s3UploadError = null;
|
||||
|
||||
// Step 1: Create local backup (always dump all databases)
|
||||
try {
|
||||
if (str($databaseType)->contains('postgres')) {
|
||||
$this->backup_file = '/pg-dump-all-'.Carbon::now()->timestamp.'.sql.gz';
|
||||
$this->backup_location = $this->backup_dir.$this->backup_file;
|
||||
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
||||
'uuid' => $this->backup_log_uuid,
|
||||
'filename' => $this->backup_location,
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
'local_storage_deleted' => false,
|
||||
]);
|
||||
$this->backup_standalone_postgresql();
|
||||
} elseif (str($databaseType)->contains('mongo')) {
|
||||
$this->backup_file = '/mongo-dump-all-'.Carbon::now()->timestamp.'.tar.gz';
|
||||
$this->backup_location = $this->backup_dir.$this->backup_file;
|
||||
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
||||
'uuid' => $this->backup_log_uuid,
|
||||
'filename' => $this->backup_location,
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
'local_storage_deleted' => false,
|
||||
]);
|
||||
$this->backup_standalone_mongodb();
|
||||
} elseif (str($databaseType)->contains('mysql')) {
|
||||
$this->backup_file = '/mysql-dump-all-'.Carbon::now()->timestamp.'.sql.gz';
|
||||
$this->backup_location = $this->backup_dir.$this->backup_file;
|
||||
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
||||
'uuid' => $this->backup_log_uuid,
|
||||
'filename' => $this->backup_location,
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
'local_storage_deleted' => false,
|
||||
]);
|
||||
$this->backup_standalone_mysql();
|
||||
} elseif (str($databaseType)->contains('mariadb')) {
|
||||
$this->backup_file = '/mariadb-dump-all-'.Carbon::now()->timestamp.'.sql.gz';
|
||||
$this->backup_location = $this->backup_dir.$this->backup_file;
|
||||
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
||||
'uuid' => $this->backup_log_uuid,
|
||||
'filename' => $this->backup_location,
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
'local_storage_deleted' => false,
|
||||
]);
|
||||
$this->backup_standalone_mariadb();
|
||||
} else {
|
||||
throw new \Exception('Unsupported database type');
|
||||
}
|
||||
|
||||
$size = $this->calculate_size();
|
||||
|
||||
// Verify local backup succeeded
|
||||
if ($size > 0) {
|
||||
$localBackupSucceeded = true;
|
||||
} else {
|
||||
throw new \Exception('Local backup file is empty or was not created');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Local backup failed
|
||||
if ($this->backup_log) {
|
||||
$this->backup_log->update([
|
||||
'status' => 'failed',
|
||||
'message' => $this->error_output ?? $this->backup_output ?? $e->getMessage(),
|
||||
'size' => $size,
|
||||
'filename' => null,
|
||||
's3_uploaded' => null,
|
||||
]);
|
||||
}
|
||||
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->error_output ?? $this->backup_output ?? $e->getMessage()));
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Step 2: Upload to S3 if enabled (independent of local backup)
|
||||
$localStorageDeleted = false;
|
||||
if ($this->backup->save_s3 && $localBackupSucceeded) {
|
||||
try {
|
||||
if (str($databaseType)->contains('postgres')) {
|
||||
$this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp';
|
||||
if ($this->backup->dump_all) {
|
||||
$this->backup_file = '/pg-dump-all-'.Carbon::now()->timestamp.'.gz';
|
||||
}
|
||||
$this->backup_location = $this->backup_dir.$this->backup_file;
|
||||
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
||||
'uuid' => $this->backup_log_uuid,
|
||||
'database_name' => $database,
|
||||
'filename' => $this->backup_location,
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
'local_storage_deleted' => false,
|
||||
]);
|
||||
$this->backup_standalone_postgresql($database);
|
||||
} elseif (str($databaseType)->contains('mongo')) {
|
||||
if ($database === '*') {
|
||||
$database = 'all';
|
||||
$databaseName = 'all';
|
||||
} else {
|
||||
if (str($database)->contains(':')) {
|
||||
$databaseName = str($database)->before(':');
|
||||
} else {
|
||||
$databaseName = $database;
|
||||
}
|
||||
}
|
||||
$this->backup_file = "/mongo-dump-$databaseName-".Carbon::now()->timestamp.'.tar.gz';
|
||||
$this->backup_location = $this->backup_dir.$this->backup_file;
|
||||
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
||||
'uuid' => $this->backup_log_uuid,
|
||||
'database_name' => $databaseName,
|
||||
'filename' => $this->backup_location,
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
'local_storage_deleted' => false,
|
||||
]);
|
||||
$this->backup_standalone_mongodb($database);
|
||||
} elseif (str($databaseType)->contains('mysql')) {
|
||||
$this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp';
|
||||
if ($this->backup->dump_all) {
|
||||
$this->backup_file = '/mysql-dump-all-'.Carbon::now()->timestamp.'.gz';
|
||||
}
|
||||
$this->backup_location = $this->backup_dir.$this->backup_file;
|
||||
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
||||
'uuid' => $this->backup_log_uuid,
|
||||
'database_name' => $database,
|
||||
'filename' => $this->backup_location,
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
'local_storage_deleted' => false,
|
||||
]);
|
||||
$this->backup_standalone_mysql($database);
|
||||
} elseif (str($databaseType)->contains('mariadb')) {
|
||||
$this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp';
|
||||
if ($this->backup->dump_all) {
|
||||
$this->backup_file = '/mariadb-dump-all-'.Carbon::now()->timestamp.'.gz';
|
||||
}
|
||||
$this->backup_location = $this->backup_dir.$this->backup_file;
|
||||
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
||||
'uuid' => $this->backup_log_uuid,
|
||||
'database_name' => $database,
|
||||
'filename' => $this->backup_location,
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
'local_storage_deleted' => false,
|
||||
]);
|
||||
$this->backup_standalone_mariadb($database);
|
||||
} else {
|
||||
throw new \Exception('Unsupported database type');
|
||||
}
|
||||
$this->upload_to_s3();
|
||||
|
||||
$size = $this->calculate_size();
|
||||
|
||||
// Verify local backup succeeded
|
||||
if ($size > 0) {
|
||||
$localBackupSucceeded = true;
|
||||
} else {
|
||||
throw new \Exception('Local backup file is empty or was not created');
|
||||
// If local backup is disabled, delete the local file immediately after S3 upload
|
||||
if ($this->backup->disable_local_backup) {
|
||||
deleteBackupsLocally($this->backup_location, $this->server);
|
||||
$localStorageDeleted = true;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Local backup failed
|
||||
if ($this->backup_log) {
|
||||
$this->backup_log->update([
|
||||
'status' => 'failed',
|
||||
'message' => $this->error_output ?? $this->backup_output ?? $e->getMessage(),
|
||||
'size' => $size,
|
||||
'filename' => null,
|
||||
's3_uploaded' => null,
|
||||
]);
|
||||
}
|
||||
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->error_output ?? $this->backup_output ?? $e->getMessage(), $database));
|
||||
// S3 upload failed but local backup succeeded
|
||||
$s3UploadError = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
// Step 3: Update status and send notifications based on results
|
||||
if ($localBackupSucceeded) {
|
||||
$message = $this->backup_output;
|
||||
|
||||
if ($s3UploadError) {
|
||||
$message = $message
|
||||
? $message."\n\nWarning: S3 upload failed: ".$s3UploadError
|
||||
: 'Warning: S3 upload failed: '.$s3UploadError;
|
||||
}
|
||||
|
||||
// Step 2: Upload to S3 if enabled (independent of local backup)
|
||||
$localStorageDeleted = false;
|
||||
if ($this->backup->save_s3 && $localBackupSucceeded) {
|
||||
try {
|
||||
$this->upload_to_s3();
|
||||
$this->backup_log->update([
|
||||
'status' => 'success',
|
||||
'message' => $message,
|
||||
'size' => $size,
|
||||
's3_uploaded' => $this->backup->save_s3 ? $this->s3_uploaded : null,
|
||||
'local_storage_deleted' => $localStorageDeleted,
|
||||
]);
|
||||
|
||||
// If local backup is disabled, delete the local file immediately after S3 upload
|
||||
if ($this->backup->disable_local_backup) {
|
||||
deleteBackupsLocally($this->backup_location, $this->server);
|
||||
$localStorageDeleted = true;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// S3 upload failed but local backup succeeded
|
||||
$s3UploadError = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Update status and send notifications based on results
|
||||
if ($localBackupSucceeded) {
|
||||
$message = $this->backup_output;
|
||||
|
||||
if ($s3UploadError) {
|
||||
$message = $message
|
||||
? $message."\n\nWarning: S3 upload failed: ".$s3UploadError
|
||||
: 'Warning: S3 upload failed: '.$s3UploadError;
|
||||
}
|
||||
|
||||
$this->backup_log->update([
|
||||
'status' => 'success',
|
||||
'message' => $message,
|
||||
'size' => $size,
|
||||
's3_uploaded' => $this->backup->save_s3 ? $this->s3_uploaded : null,
|
||||
'local_storage_deleted' => $localStorageDeleted,
|
||||
]);
|
||||
|
||||
// Send appropriate notification
|
||||
if ($s3UploadError) {
|
||||
$this->team->notify(new BackupSuccessWithS3Warning($this->backup, $this->database, $database, $s3UploadError));
|
||||
} else {
|
||||
$this->team->notify(new BackupSuccess($this->backup, $this->database, $database));
|
||||
}
|
||||
// Send appropriate notification
|
||||
if ($s3UploadError) {
|
||||
$this->team->notify(new BackupSuccessWithS3Warning($this->backup, $this->database, $s3UploadError));
|
||||
} else {
|
||||
$this->team->notify(new BackupSuccess($this->backup, $this->database));
|
||||
}
|
||||
}
|
||||
if ($this->backup_log && $this->backup_log->status === 'success') {
|
||||
|
|
@ -458,7 +357,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
}
|
||||
}
|
||||
|
||||
private function backup_standalone_mongodb(string $databaseWithCollections): void
|
||||
private function backup_standalone_mongodb(): void
|
||||
{
|
||||
try {
|
||||
$url = $this->database->internal_db_url;
|
||||
|
|
@ -473,41 +372,14 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
}
|
||||
}
|
||||
\Log::info('MongoDB backup URL configured', ['has_url' => filled($url), 'using_env_vars' => blank($this->database->internal_db_url)]);
|
||||
if ($databaseWithCollections === 'all') {
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
if (str($this->database->image)->startsWith('mongo:4')) {
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=\"$url\" --gzip --archive > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --gzip --archive > $this->backup_location";
|
||||
}
|
||||
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
if (str($this->database->image)->startsWith('mongo:4')) {
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=\"$url\" --gzip --archive > $this->backup_location";
|
||||
} else {
|
||||
if (str($databaseWithCollections)->contains(':')) {
|
||||
$databaseName = str($databaseWithCollections)->before(':');
|
||||
$collectionsToExclude = str($databaseWithCollections)->after(':')->explode(',');
|
||||
} else {
|
||||
$databaseName = $databaseWithCollections;
|
||||
$collectionsToExclude = collect();
|
||||
}
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
|
||||
// Validate and escape database name to prevent command injection
|
||||
validateShellSafePath($databaseName, 'database name');
|
||||
$escapedDatabaseName = escapeshellarg($databaseName);
|
||||
|
||||
if ($collectionsToExclude->count() === 0) {
|
||||
if (str($this->database->image)->startsWith('mongo:4')) {
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=\"$url\" --gzip --archive > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $escapedDatabaseName --gzip --archive > $this->backup_location";
|
||||
}
|
||||
} else {
|
||||
if (str($this->database->image)->startsWith('mongo:4')) {
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $escapedDatabaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
|
||||
}
|
||||
}
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --gzip --archive > $this->backup_location";
|
||||
}
|
||||
|
||||
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
if ($this->backup_output === '') {
|
||||
|
|
@ -519,7 +391,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
}
|
||||
}
|
||||
|
||||
private function backup_standalone_postgresql(string $database): void
|
||||
private function backup_standalone_postgresql(): void
|
||||
{
|
||||
try {
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
|
|
@ -527,14 +399,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
if ($this->postgres_password) {
|
||||
$backupCommand .= " -e PGPASSWORD=\"{$this->postgres_password}\"";
|
||||
}
|
||||
if ($this->backup->dump_all) {
|
||||
$backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location";
|
||||
} else {
|
||||
// Validate and escape database name to prevent command injection
|
||||
validateShellSafePath($database, 'database name');
|
||||
$escapedDatabase = escapeshellarg($database);
|
||||
$backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $escapedDatabase > $this->backup_location";
|
||||
}
|
||||
$backupCommand .= " $this->container_name pg_dumpall --clean --if-exists --username {$this->database->postgres_user} | gzip > $this->backup_location";
|
||||
|
||||
$commands[] = $backupCommand;
|
||||
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
|
||||
|
|
@ -548,18 +413,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
}
|
||||
}
|
||||
|
||||
private function backup_standalone_mysql(string $database): void
|
||||
private function backup_standalone_mysql(): void
|
||||
{
|
||||
try {
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
if ($this->backup->dump_all) {
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location";
|
||||
} else {
|
||||
// Validate and escape database name to prevent command injection
|
||||
validateShellSafePath($database, 'database name');
|
||||
$escapedDatabase = escapeshellarg($database);
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" $escapedDatabase > $this->backup_location";
|
||||
}
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" --all-databases --single-transaction --quick --lock-tables=false | gzip > $this->backup_location";
|
||||
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
if ($this->backup_output === '') {
|
||||
|
|
@ -571,18 +429,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
}
|
||||
}
|
||||
|
||||
private function backup_standalone_mariadb(string $database): void
|
||||
private function backup_standalone_mariadb(): void
|
||||
{
|
||||
try {
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
if ($this->backup->dump_all) {
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location";
|
||||
} else {
|
||||
// Validate and escape database name to prevent command injection
|
||||
validateShellSafePath($database, 'database name');
|
||||
$escapedDatabase = escapeshellarg($database);
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" $escapedDatabase > $this->backup_location";
|
||||
}
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" --all-databases --single-transaction --quick --lock-tables=false | gzip > $this->backup_location";
|
||||
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
if ($this->backup_output === '') {
|
||||
|
|
@ -709,9 +560,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
// Notify team about permanent failure
|
||||
if ($this->team) {
|
||||
$databaseName = $log?->database_name ?? 'unknown';
|
||||
$output = $this->backup_output ?? $exception?->getMessage() ?? 'Unknown error';
|
||||
$this->team->notify(new BackupFailed($this->backup, $this->database, $output, $databaseName));
|
||||
$this->team->notify(new BackupFailed($this->backup, $this->database, $output));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,12 +69,6 @@ class BackupEdit extends Component
|
|||
#[Validate(['nullable', 'integer'])]
|
||||
public ?int $s3StorageId = 1;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $databasesToBackup = null;
|
||||
|
||||
#[Validate(['required', 'boolean'])]
|
||||
public bool $dumpAll = false;
|
||||
|
||||
#[Validate(['required', 'int', 'min:60', 'max:36000'])]
|
||||
public int $timeout = 3600;
|
||||
|
||||
|
|
@ -103,27 +97,6 @@ class BackupEdit extends Component
|
|||
$this->backup->save_s3 = $this->saveS3;
|
||||
$this->backup->disable_local_backup = $this->disableLocalBackup;
|
||||
$this->backup->s3_storage_id = $this->s3StorageId;
|
||||
|
||||
// Validate databases_to_backup to prevent command injection
|
||||
if (filled($this->databasesToBackup)) {
|
||||
$databases = str($this->databasesToBackup)->explode(',');
|
||||
foreach ($databases as $index => $db) {
|
||||
$dbName = trim($db);
|
||||
try {
|
||||
validateShellSafePath($dbName, 'database name');
|
||||
} catch (\Exception $e) {
|
||||
// Provide specific error message indicating which database failed validation
|
||||
$position = $index + 1;
|
||||
throw new \Exception(
|
||||
"Database #{$position} ('{$dbName}') validation failed: ".
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->backup->databases_to_backup = $this->databasesToBackup;
|
||||
$this->backup->dump_all = $this->dumpAll;
|
||||
$this->backup->timeout = $this->timeout;
|
||||
$this->customValidate();
|
||||
$this->backup->save();
|
||||
|
|
@ -140,8 +113,6 @@ class BackupEdit extends Component
|
|||
$this->saveS3 = $this->backup->save_s3;
|
||||
$this->disableLocalBackup = $this->backup->disable_local_backup ?? false;
|
||||
$this->s3StorageId = $this->backup->s3_storage_id;
|
||||
$this->databasesToBackup = $this->backup->databases_to_backup;
|
||||
$this->dumpAll = $this->backup->dump_all;
|
||||
$this->timeout = $this->backup->timeout;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,14 +65,6 @@ class CreateScheduledBackup extends Component
|
|||
'team_id' => currentTeam()->id,
|
||||
];
|
||||
|
||||
if ($this->database->type() === 'standalone-postgresql') {
|
||||
$payload['databases_to_backup'] = $this->database->postgres_db;
|
||||
} elseif ($this->database->type() === 'standalone-mysql') {
|
||||
$payload['databases_to_backup'] = $this->database->mysql_database;
|
||||
} elseif ($this->database->type() === 'standalone-mariadb') {
|
||||
$payload['databases_to_backup'] = $this->database->mariadb_database;
|
||||
}
|
||||
|
||||
$databaseBackup = ScheduledDatabaseBackup::create($payload);
|
||||
if ($this->database->getMorphClass() === \App\Models\ServiceDatabase::class) {
|
||||
$this->dispatch('refreshScheduledBackups', $databaseBackup->id);
|
||||
|
|
|
|||
|
|
@ -139,7 +139,9 @@ class Import extends Component
|
|||
|
||||
public array $importCommands = [];
|
||||
|
||||
public bool $dumpAll = false;
|
||||
public bool $legacyDumpAll = false;
|
||||
|
||||
public bool $legacySingleDb = false;
|
||||
|
||||
public string $restoreCommandText = '';
|
||||
|
||||
|
|
@ -147,11 +149,11 @@ class Import extends Component
|
|||
|
||||
public ?int $activityId = null;
|
||||
|
||||
public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}';
|
||||
public string $postgresqlRestoreCommand = 'gunzip -c $tmpPath | psql -X -U ${POSTGRES_USER} -d postgres';
|
||||
|
||||
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
|
||||
public string $mysqlRestoreCommand = 'gunzip -c $tmpPath | mysql -u root -p$MYSQL_ROOT_PASSWORD';
|
||||
|
||||
public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
|
||||
public string $mariadbRestoreCommand = 'gunzip -c $tmpPath | mariadb -u root -p$MARIADB_ROOT_PASSWORD';
|
||||
|
||||
public string $mongodbRestoreCommand = 'mongorestore --authenticationDatabase=admin --username $MONGO_INITDB_ROOT_USERNAME --password $MONGO_INITDB_ROOT_PASSWORD --uri mongodb://localhost:27017 --gzip --archive=';
|
||||
|
||||
|
|
@ -206,7 +208,23 @@ class Import extends Component
|
|||
$this->loadAvailableS3Storages();
|
||||
}
|
||||
|
||||
public function updatedDumpAll($value)
|
||||
public function updatedLegacyDumpAll($value)
|
||||
{
|
||||
if ($value) {
|
||||
$this->legacySingleDb = false;
|
||||
}
|
||||
$this->updateRestoreCommands();
|
||||
}
|
||||
|
||||
public function updatedLegacySingleDb($value)
|
||||
{
|
||||
if ($value) {
|
||||
$this->legacyDumpAll = false;
|
||||
}
|
||||
$this->updateRestoreCommands();
|
||||
}
|
||||
|
||||
private function updateRestoreCommands()
|
||||
{
|
||||
$morphClass = $this->resource->getMorphClass();
|
||||
|
||||
|
|
@ -225,7 +243,7 @@ class Import extends Component
|
|||
switch ($morphClass) {
|
||||
case \App\Models\StandaloneMariadb::class:
|
||||
case 'mariadb':
|
||||
if ($value === true) {
|
||||
if ($this->legacyDumpAll) {
|
||||
$this->mariadbRestoreCommand = <<<'EOD'
|
||||
for pid in $(mariadb -u root -p$MARIADB_ROOT_PASSWORD -N -e "SELECT id FROM information_schema.processlist WHERE user != 'root';"); do
|
||||
mariadb -u root -p$MARIADB_ROOT_PASSWORD -e "KILL $pid" 2>/dev/null || true
|
||||
|
|
@ -234,14 +252,15 @@ mariadb -u root -p$MARIADB_ROOT_PASSWORD -N -e "SELECT CONCAT('DROP DATABASE IF
|
|||
mariadb -u root -p$MARIADB_ROOT_PASSWORD -e "CREATE DATABASE IF NOT EXISTS \`${MARIADB_DATABASE:-default}\`;" && \
|
||||
(gunzip -cf $tmpPath 2>/dev/null || cat $tmpPath) | sed -e '/^CREATE DATABASE/d' -e '/^USE \`mysql\`/d' | mariadb -u root -p$MARIADB_ROOT_PASSWORD ${MARIADB_DATABASE:-default}
|
||||
EOD;
|
||||
$this->restoreCommandText = $this->mariadbRestoreCommand.' && (gunzip -cf <temp_backup_file> 2>/dev/null || cat <temp_backup_file>) | mariadb -u root -p$MARIADB_ROOT_PASSWORD ${MARIADB_DATABASE:-default}';
|
||||
} elseif ($this->legacySingleDb) {
|
||||
$this->mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE < $tmpPath';
|
||||
} else {
|
||||
$this->mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
|
||||
$this->mariadbRestoreCommand = 'gunzip -c $tmpPath | mariadb -u root -p$MARIADB_ROOT_PASSWORD';
|
||||
}
|
||||
break;
|
||||
case \App\Models\StandaloneMysql::class:
|
||||
case 'mysql':
|
||||
if ($value === true) {
|
||||
if ($this->legacyDumpAll) {
|
||||
$this->mysqlRestoreCommand = <<<'EOD'
|
||||
for pid in $(mysql -u root -p$MYSQL_ROOT_PASSWORD -N -e "SELECT id FROM information_schema.processlist WHERE user != 'root';"); do
|
||||
mysql -u root -p$MYSQL_ROOT_PASSWORD -e "KILL $pid" 2>/dev/null || true
|
||||
|
|
@ -250,26 +269,29 @@ mysql -u root -p$MYSQL_ROOT_PASSWORD -N -e "SELECT CONCAT('DROP DATABASE IF EXIS
|
|||
mysql -u root -p$MYSQL_ROOT_PASSWORD -e "CREATE DATABASE IF NOT EXISTS \`${MYSQL_DATABASE:-default}\`;" && \
|
||||
(gunzip -cf $tmpPath 2>/dev/null || cat $tmpPath) | sed -e '/^CREATE DATABASE/d' -e '/^USE \`mysql\`/d' | mysql -u root -p$MYSQL_ROOT_PASSWORD ${MYSQL_DATABASE:-default}
|
||||
EOD;
|
||||
$this->restoreCommandText = $this->mysqlRestoreCommand.' && (gunzip -cf <temp_backup_file> 2>/dev/null || cat <temp_backup_file>) | mysql -u root -p$MYSQL_ROOT_PASSWORD ${MYSQL_DATABASE:-default}';
|
||||
} elseif ($this->legacySingleDb) {
|
||||
$this->mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE < $tmpPath';
|
||||
} else {
|
||||
$this->mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
|
||||
$this->mysqlRestoreCommand = 'gunzip -c $tmpPath | mysql -u root -p$MYSQL_ROOT_PASSWORD';
|
||||
}
|
||||
break;
|
||||
case \App\Models\StandalonePostgresql::class:
|
||||
case 'postgresql':
|
||||
if ($value === true) {
|
||||
if ($this->legacyDumpAll) {
|
||||
$this->postgresqlRestoreCommand = <<<'EOD'
|
||||
psql -U ${POSTGRES_USER} -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname IS NOT NULL AND pid <> pg_backend_pid()" && \
|
||||
psql -U ${POSTGRES_USER} -t -c "SELECT datname FROM pg_database WHERE NOT datistemplate" | xargs -I {} dropdb -U ${POSTGRES_USER} --if-exists {} && \
|
||||
createdb -U ${POSTGRES_USER} ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}
|
||||
createdb -U ${POSTGRES_USER} ${POSTGRES_DB:-${POSTGRES_USER:-postgres}} && \
|
||||
(gunzip -cf $tmpPath 2>/dev/null || cat $tmpPath) | psql -U ${POSTGRES_USER} -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}
|
||||
EOD;
|
||||
$this->restoreCommandText = $this->postgresqlRestoreCommand.' && (gunzip -cf <temp_backup_file> 2>/dev/null || cat <temp_backup_file>) | psql -U ${POSTGRES_USER} -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}';
|
||||
} elseif ($this->legacySingleDb) {
|
||||
$this->postgresqlRestoreCommand = 'pg_restore -U ${POSTGRES_USER} -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}} $tmpPath';
|
||||
} else {
|
||||
$this->postgresqlRestoreCommand = 'pg_restore -U ${POSTGRES_USER} -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}';
|
||||
$this->postgresqlRestoreCommand = 'gunzip -c $tmpPath | psql -X -U ${POSTGRES_USER} -d postgres';
|
||||
$this->restoreCommandText = 'Default: Restore from pg_dumpall file (gzipped SQL)';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function getContainers()
|
||||
|
|
@ -737,37 +759,19 @@ EOD;
|
|||
switch ($morphClass) {
|
||||
case \App\Models\StandaloneMariadb::class:
|
||||
case 'mariadb':
|
||||
$restoreCommand = $this->mariadbRestoreCommand;
|
||||
if ($this->dumpAll) {
|
||||
$restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | mariadb -u root -p\$MARIADB_ROOT_PASSWORD \${MARIADB_DATABASE:-default}";
|
||||
} else {
|
||||
$restoreCommand .= " < {$tmpPath}";
|
||||
}
|
||||
$restoreCommand = str_replace('$tmpPath', $tmpPath, $this->mariadbRestoreCommand);
|
||||
break;
|
||||
case \App\Models\StandaloneMysql::class:
|
||||
case 'mysql':
|
||||
$restoreCommand = $this->mysqlRestoreCommand;
|
||||
if ($this->dumpAll) {
|
||||
$restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | mysql -u root -p\$MYSQL_ROOT_PASSWORD \${MYSQL_DATABASE:-default}";
|
||||
} else {
|
||||
$restoreCommand .= " < {$tmpPath}";
|
||||
}
|
||||
$restoreCommand = str_replace('$tmpPath', $tmpPath, $this->mysqlRestoreCommand);
|
||||
break;
|
||||
case \App\Models\StandalonePostgresql::class:
|
||||
case 'postgresql':
|
||||
$restoreCommand = $this->postgresqlRestoreCommand;
|
||||
if ($this->dumpAll) {
|
||||
$restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | psql -U \${POSTGRES_USER} -d \${POSTGRES_DB:-\${POSTGRES_USER:-postgres}}";
|
||||
} else {
|
||||
$restoreCommand .= " {$tmpPath}";
|
||||
}
|
||||
$restoreCommand = str_replace('$tmpPath', $tmpPath, $this->postgresqlRestoreCommand);
|
||||
break;
|
||||
case \App\Models\StandaloneMongodb::class:
|
||||
case 'mongodb':
|
||||
$restoreCommand = $this->mongodbRestoreCommand;
|
||||
if ($this->dumpAll === false) {
|
||||
$restoreCommand .= "{$tmpPath}";
|
||||
}
|
||||
$restoreCommand = $this->mongodbRestoreCommand.$tmpPath;
|
||||
break;
|
||||
default:
|
||||
$restoreCommand = '';
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class ScheduledDatabaseBackupExecution extends BaseModel
|
|||
's3_uploaded' => 'boolean',
|
||||
'local_storage_deleted' => 'boolean',
|
||||
's3_storage_deleted' => 'boolean',
|
||||
'legacy' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class BackupFailed extends CustomEmailNotification
|
|||
|
||||
public string $frequency;
|
||||
|
||||
public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output, public $database_name)
|
||||
public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->name = $database->name;
|
||||
|
|
@ -33,7 +33,6 @@ class BackupFailed extends CustomEmailNotification
|
|||
$mail->subject("Coolify: [ACTION REQUIRED] Database Backup FAILED for {$this->database->name}");
|
||||
$mail->view('emails.backup-failed', [
|
||||
'name' => $this->name,
|
||||
'database_name' => $this->database_name,
|
||||
'frequency' => $this->frequency,
|
||||
'output' => $this->output,
|
||||
]);
|
||||
|
|
@ -45,7 +44,7 @@ class BackupFailed extends CustomEmailNotification
|
|||
{
|
||||
$message = new DiscordMessage(
|
||||
title: ':cross_mark: Database backup failed',
|
||||
description: "Database backup for {$this->name} (db:{$this->database_name}) has FAILED.",
|
||||
description: "Database backup for {$this->name} has FAILED.",
|
||||
color: DiscordMessage::errorColor(),
|
||||
isCritical: true,
|
||||
);
|
||||
|
|
@ -58,7 +57,7 @@ class BackupFailed extends CustomEmailNotification
|
|||
|
||||
public function toTelegram(): array
|
||||
{
|
||||
$message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}";
|
||||
$message = "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}";
|
||||
|
||||
return [
|
||||
'message' => $message,
|
||||
|
|
@ -70,14 +69,14 @@ class BackupFailed extends CustomEmailNotification
|
|||
return new PushoverMessage(
|
||||
title: 'Database backup failed',
|
||||
level: 'error',
|
||||
message: "Database backup for {$this->name} (db:{$this->database_name}) was FAILED<br/><br/><b>Frequency:</b> {$this->frequency} .<br/><b>Reason:</b> {$this->output}",
|
||||
message: "Database backup for {$this->name} was FAILED<br/><br/><b>Frequency:</b> {$this->frequency} .<br/><b>Reason:</b> {$this->output}",
|
||||
);
|
||||
}
|
||||
|
||||
public function toSlack(): SlackMessage
|
||||
{
|
||||
$title = 'Database backup failed';
|
||||
$description = "Database backup for {$this->name} (db:{$this->database_name}) has FAILED.";
|
||||
$description = "Database backup for {$this->name} has FAILED.";
|
||||
|
||||
$description .= "\n\n*Frequency:* {$this->frequency}";
|
||||
$description .= "\n\n*Error Output:* {$this->output}";
|
||||
|
|
@ -99,7 +98,6 @@ class BackupFailed extends CustomEmailNotification
|
|||
'event' => 'backup_failed',
|
||||
'database_name' => $this->name,
|
||||
'database_uuid' => $this->database->uuid,
|
||||
'database_type' => $this->database_name,
|
||||
'frequency' => $this->frequency,
|
||||
'error_output' => $this->output,
|
||||
'url' => $url,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class BackupSuccess extends CustomEmailNotification
|
|||
|
||||
public string $frequency;
|
||||
|
||||
public function __construct(ScheduledDatabaseBackup $backup, public $database, public $database_name)
|
||||
public function __construct(ScheduledDatabaseBackup $backup, public $database)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
|
||||
|
|
@ -34,7 +34,6 @@ class BackupSuccess extends CustomEmailNotification
|
|||
$mail->subject("Coolify: Backup successfully done for {$this->database->name}");
|
||||
$mail->view('emails.backup-success', [
|
||||
'name' => $this->name,
|
||||
'database_name' => $this->database_name,
|
||||
'frequency' => $this->frequency,
|
||||
]);
|
||||
|
||||
|
|
@ -45,7 +44,7 @@ class BackupSuccess extends CustomEmailNotification
|
|||
{
|
||||
$message = new DiscordMessage(
|
||||
title: ':white_check_mark: Database backup successful',
|
||||
description: "Database backup for {$this->name} (db:{$this->database_name}) was successful.",
|
||||
description: "Database backup for {$this->name} was successful.",
|
||||
color: DiscordMessage::successColor(),
|
||||
);
|
||||
|
||||
|
|
@ -56,7 +55,7 @@ class BackupSuccess extends CustomEmailNotification
|
|||
|
||||
public function toTelegram(): array
|
||||
{
|
||||
$message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful.";
|
||||
$message = "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
|
||||
|
||||
return [
|
||||
'message' => $message,
|
||||
|
|
@ -68,14 +67,14 @@ class BackupSuccess extends CustomEmailNotification
|
|||
return new PushoverMessage(
|
||||
title: 'Database backup successful',
|
||||
level: 'success',
|
||||
message: "Database backup for {$this->name} (db:{$this->database_name}) was successful.<br/><br/><b>Frequency:</b> {$this->frequency}.",
|
||||
message: "Database backup for {$this->name} was successful.<br/><br/><b>Frequency:</b> {$this->frequency}.",
|
||||
);
|
||||
}
|
||||
|
||||
public function toSlack(): SlackMessage
|
||||
{
|
||||
$title = 'Database backup successful';
|
||||
$description = "Database backup for {$this->name} (db:{$this->database_name}) was successful.";
|
||||
$description = "Database backup for {$this->name} was successful.";
|
||||
|
||||
$description .= "\n\n*Frequency:* {$this->frequency}";
|
||||
|
||||
|
|
@ -96,7 +95,6 @@ class BackupSuccess extends CustomEmailNotification
|
|||
'event' => 'backup_success',
|
||||
'database_name' => $this->name,
|
||||
'database_uuid' => $this->database->uuid,
|
||||
'database_type' => $this->database_name,
|
||||
'frequency' => $this->frequency,
|
||||
'url' => $url,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification
|
|||
|
||||
public ?string $s3_storage_url = null;
|
||||
|
||||
public function __construct(ScheduledDatabaseBackup $backup, public $database, public $database_name, public $s3_error)
|
||||
public function __construct(ScheduledDatabaseBackup $backup, public $database, public $s3_error)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
|
||||
|
|
@ -40,7 +40,6 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification
|
|||
$mail->subject("Coolify: Backup succeeded locally but S3 upload failed for {$this->database->name}");
|
||||
$mail->view('emails.backup-success-with-s3-warning', [
|
||||
'name' => $this->name,
|
||||
'database_name' => $this->database_name,
|
||||
'frequency' => $this->frequency,
|
||||
's3_error' => $this->s3_error,
|
||||
's3_storage_url' => $this->s3_storage_url,
|
||||
|
|
@ -53,7 +52,7 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification
|
|||
{
|
||||
$message = new DiscordMessage(
|
||||
title: ':warning: Database backup succeeded locally, S3 upload failed',
|
||||
description: "Database backup for {$this->name} (db:{$this->database_name}) was created successfully on local storage, but failed to upload to S3.",
|
||||
description: "Database backup for {$this->name} was created successfully on local storage, but failed to upload to S3.",
|
||||
color: DiscordMessage::warningColor(),
|
||||
);
|
||||
|
||||
|
|
@ -69,7 +68,7 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification
|
|||
|
||||
public function toTelegram(): array
|
||||
{
|
||||
$message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} succeeded locally but failed to upload to S3.\n\nS3 Error:\n{$this->s3_error}";
|
||||
$message = "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} succeeded locally but failed to upload to S3.\n\nS3 Error:\n{$this->s3_error}";
|
||||
|
||||
if ($this->s3_storage_url) {
|
||||
$message .= "\n\nCheck S3 Configuration: {$this->s3_storage_url}";
|
||||
|
|
@ -82,7 +81,7 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification
|
|||
|
||||
public function toPushover(): PushoverMessage
|
||||
{
|
||||
$message = "Database backup for {$this->name} (db:{$this->database_name}) was created successfully on local storage, but failed to upload to S3.<br/><br/><b>Frequency:</b> {$this->frequency}.<br/><b>S3 Error:</b> {$this->s3_error}";
|
||||
$message = "Database backup for {$this->name} was created successfully on local storage, but failed to upload to S3.<br/><br/><b>Frequency:</b> {$this->frequency}.<br/><b>S3 Error:</b> {$this->s3_error}";
|
||||
|
||||
if ($this->s3_storage_url) {
|
||||
$message .= "<br/><br/><a href=\"{$this->s3_storage_url}\">Check S3 Configuration</a>";
|
||||
|
|
@ -98,7 +97,7 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification
|
|||
public function toSlack(): SlackMessage
|
||||
{
|
||||
$title = 'Database backup succeeded locally, S3 upload failed';
|
||||
$description = "Database backup for {$this->name} (db:{$this->database_name}) was created successfully on local storage, but failed to upload to S3.";
|
||||
$description = "Database backup for {$this->name} was created successfully on local storage, but failed to upload to S3.";
|
||||
|
||||
$description .= "\n\n*Frequency:* {$this->frequency}";
|
||||
$description .= "\n\n*S3 Error:* {$this->s3_error}";
|
||||
|
|
@ -124,7 +123,6 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification
|
|||
'event' => 'backup_success_with_s3_warning',
|
||||
'database_name' => $this->name,
|
||||
'database_uuid' => $this->database->uuid,
|
||||
'database_type' => $this->database_name,
|
||||
'frequency' => $this->frequency,
|
||||
's3_error' => $this->s3_error,
|
||||
'url' => $url,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('scheduled_database_backup_executions', function (Blueprint $table) {
|
||||
$table->boolean('legacy')->default(false)->after('filename');
|
||||
});
|
||||
|
||||
// Mark all existing backup executions as legacy
|
||||
DB::table('scheduled_database_backup_executions')->update(['legacy' => true]);
|
||||
|
||||
// @todo: In a future update or v5 we no longer need "databases_to_backup", "dump_all" in "scheduled_database_backups" - keeping them for now if we need to rollback.
|
||||
// @todo: In a future update or v5 we no longer need "database_name" from "scheduled_database_backup_executions" - keeping them for now if we need to rollback.
|
||||
}
|
||||
};
|
||||
17
openapi.json
17
openapi.json
|
|
@ -3940,15 +3940,6 @@
|
|||
"type": "string",
|
||||
"description": "S3 storage UUID (required if save_s3 is true)"
|
||||
},
|
||||
"databases_to_backup": {
|
||||
"type": "string",
|
||||
"description": "Comma separated list of databases to backup"
|
||||
},
|
||||
"dump_all": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to dump all databases",
|
||||
"default": false
|
||||
},
|
||||
"backup_now": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to trigger backup immediately after creation"
|
||||
|
|
@ -4502,14 +4493,6 @@
|
|||
"type": "boolean",
|
||||
"description": "Whether the backup is enabled or not"
|
||||
},
|
||||
"databases_to_backup": {
|
||||
"type": "string",
|
||||
"description": "Comma separated list of databases to backup"
|
||||
},
|
||||
"dump_all": {
|
||||
"type": "boolean",
|
||||
"description": "Whether all databases are dumped or not"
|
||||
},
|
||||
"frequency": {
|
||||
"type": "string",
|
||||
"description": "Frequency of the backup"
|
||||
|
|
|
|||
13
openapi.yaml
13
openapi.yaml
|
|
@ -2471,13 +2471,6 @@ paths:
|
|||
s3_storage_uuid:
|
||||
type: string
|
||||
description: 'S3 storage UUID (required if save_s3 is true)'
|
||||
databases_to_backup:
|
||||
type: string
|
||||
description: 'Comma separated list of databases to backup'
|
||||
dump_all:
|
||||
type: boolean
|
||||
description: 'Whether to dump all databases'
|
||||
default: false
|
||||
backup_now:
|
||||
type: boolean
|
||||
description: 'Whether to trigger backup immediately after creation'
|
||||
|
|
@ -2860,12 +2853,6 @@ paths:
|
|||
enabled:
|
||||
type: boolean
|
||||
description: 'Whether the backup is enabled or not'
|
||||
databases_to_backup:
|
||||
type: string
|
||||
description: 'Comma separated list of databases to backup'
|
||||
dump_all:
|
||||
type: boolean
|
||||
description: 'Whether all databases are dumped or not'
|
||||
frequency:
|
||||
type: string
|
||||
description: 'Frequency of the backup'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<x-emails.layout>
|
||||
Database backup for {{ $name }} @if($database_name)(db:{{ $database_name }})@endif with frequency of {{ $frequency }} was FAILED.
|
||||
Database backup for {{ $name }} with frequency of {{ $frequency }} was FAILED.
|
||||
|
||||
### Reason
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<x-emails.layout>
|
||||
Database backup for {{ $name }} @if($database_name)(db:{{ $database_name }})@endif with frequency of {{ $frequency }} succeeded locally but failed to upload to S3.
|
||||
Database backup for {{ $name }} with frequency of {{ $frequency }} succeeded locally but failed to upload to S3.
|
||||
|
||||
S3 Error: {{ $s3_error }}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
<x-emails.layout>
|
||||
Database backup for {{ $name }} @if($database_name)(db:{{ $database_name }})@endif with frequency of {{ $frequency }} was successful.
|
||||
Database backup for {{ $name }} with frequency of {{ $frequency }} was successful.
|
||||
</x-emails.layout>
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
<x-emails.layout>
|
||||
@foreach ($databases as $database_name => $databases)
|
||||
|
||||
@if(data_get($databases,'failed_count') > 0)
|
||||
|
||||
<div style="color:red">
|
||||
|
||||
"{{ $database_name }}" backups: There were some failed backups. Please login and check the logs for more details.
|
||||
|
||||
</div>
|
||||
|
||||
@else
|
||||
|
||||
"{{ $database_name }}" backups: All backups were successful.
|
||||
|
||||
@endif
|
||||
|
||||
@endforeach
|
||||
</x-emails.layout>
|
||||
|
|
@ -46,40 +46,6 @@
|
|||
@endif
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3>Settings</h3>
|
||||
<div class="flex gap-2 flex-col ">
|
||||
@if ($backup->database_type === 'App\Models\StandalonePostgresql' && $backup->database_id !== 0)
|
||||
<div class="w-48">
|
||||
<x-forms.checkbox label="Backup All Databases" id="dumpAll" instantSave />
|
||||
</div>
|
||||
@if (!$backup->dump_all)
|
||||
<x-forms.input label="Databases To Backup"
|
||||
helper="Comma separated list of databases to backup. Empty will include the default one."
|
||||
id="databasesToBackup" />
|
||||
@endif
|
||||
@elseif($backup->database_type === 'App\Models\StandaloneMongodb')
|
||||
<x-forms.input label="Databases To Include"
|
||||
helper="A list of databases to backup. You can specify which collection(s) per database to exclude from the backup. Empty will include all databases and collections.<br><br>Example:<br><br>database1:collection1,collection2|database2:collection3,collection4<br><br> database1 will include all collections except collection1 and collection2. <br>database2 will include all collections except collection3 and collection4.<br><br>Another Example:<br><br>database1:collection1|database2<br><br> database1 will include all collections except collection1.<br>database2 will include ALL collections."
|
||||
id="databasesToBackup" />
|
||||
@elseif($backup->database_type === 'App\Models\StandaloneMysql')
|
||||
<div class="w-48">
|
||||
<x-forms.checkbox label="Backup All Databases" id="dumpAll" instantSave />
|
||||
</div>
|
||||
@if (!$backup->dump_all)
|
||||
<x-forms.input label="Databases To Backup"
|
||||
helper="Comma separated list of databases to backup. Empty will include the default one."
|
||||
id="databasesToBackup" />
|
||||
@endif
|
||||
@elseif($backup->database_type === 'App\Models\StandaloneMariadb')
|
||||
<div class="w-48">
|
||||
<x-forms.checkbox label="Backup All Databases" id="dumpAll" instantSave />
|
||||
</div>
|
||||
@if (!$backup->dump_all)
|
||||
<x-forms.input label="Databases To Backup"
|
||||
helper="Comma separated list of databases to backup. Empty will include the default one."
|
||||
id="databasesToBackup" />
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Frequency" id="frequency" />
|
||||
<x-forms.input label="Timezone" id="timezone" disabled
|
||||
|
|
|
|||
|
|
@ -66,6 +66,11 @@
|
|||
@endphp
|
||||
{{ $statusText }}
|
||||
</span>
|
||||
@if (data_get($execution, 'legacy', false))
|
||||
<span class="px-2 py-1 rounded-md text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-200">
|
||||
Legacy
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
@if (data_get($execution, 'status') === 'running')
|
||||
|
|
@ -79,7 +84,6 @@
|
|||
• {{ \Carbon\Carbon::parse(data_get($execution, 'finished_at'))->format('M j, H:i') }}
|
||||
</span>
|
||||
@endif
|
||||
• Database: {{ data_get($execution, 'database_name', 'N/A') }}
|
||||
@if(data_get($execution, 'size'))
|
||||
• Size: {{ formatBytes(data_get($execution, 'size')) }}
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -61,39 +61,49 @@
|
|||
@if (str($resourceStatus)->startsWith('running'))
|
||||
{{-- Restore Command Configuration --}}
|
||||
@if ($resourceDbType === 'standalone-postgresql')
|
||||
@if ($dumpAll)
|
||||
<x-forms.textarea rows="6" readonly label="Custom Import Command"
|
||||
wire:model='restoreCommandText'></x-forms.textarea>
|
||||
@else
|
||||
<x-forms.input label="Custom Import Command" wire:model='postgresqlRestoreCommand'></x-forms.input>
|
||||
<div class="flex flex-col gap-1 pt-1">
|
||||
<span class="text-xs">You can add "--clean" to drop objects before creating them, avoiding
|
||||
conflicts.</span>
|
||||
<span class="text-xs">You can add "--verbose" to log more things.</span>
|
||||
<div class="flex flex-col gap-2 pt-4">
|
||||
<h3>Import Options</h3>
|
||||
<div class="flex flex-col gap-1 w-96">
|
||||
<x-forms.checkbox label="Legacy Backup all databases Backups" wire:model.live='legacyDumpAll'
|
||||
helper="For legacy backups created with the 'Backup all databases' option."></x-forms.checkbox>
|
||||
<x-forms.checkbox label="Legacy Single DB Backups" wire:model.live='legacySingleDb'
|
||||
helper="Uses pg_restore for single database restores for backups with the format .dmp."></x-forms.checkbox>
|
||||
</div>
|
||||
@endif
|
||||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll'></x-forms.checkbox>
|
||||
<x-forms.input label="Restore Command" wire:model='postgresqlRestoreCommand'
|
||||
helper="Default uses psql to restore from pg_dumpall backups. Modify if needed."></x-forms.input>
|
||||
@if ($restoreCommandText)
|
||||
<div class="text-xs text-neutral-500">{{ $restoreCommandText }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@elseif ($resourceDbType === 'standalone-mysql')
|
||||
@if ($dumpAll)
|
||||
<x-forms.textarea rows="14" readonly label="Custom Import Command"
|
||||
wire:model='restoreCommandText'></x-forms.textarea>
|
||||
@else
|
||||
<x-forms.input label="Custom Import Command" wire:model='mysqlRestoreCommand'></x-forms.input>
|
||||
@endif
|
||||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll'></x-forms.checkbox>
|
||||
<div class="flex flex-col gap-2 pt-4">
|
||||
<h3>Import Options</h3>
|
||||
<div class="flex flex-col gap-1 w-96">
|
||||
<x-forms.checkbox label="Legacy Backup all databases Backups" wire:model.live='legacyDumpAll'
|
||||
helper="For legacy backups created with the 'Backup all databases' option."></x-forms.checkbox>
|
||||
<x-forms.checkbox label="Legacy Single DB Backups" wire:model.live='legacySingleDb'
|
||||
helper="Uses mysql for single database restores for backups with the format .sql."></x-forms.checkbox>
|
||||
</div>
|
||||
<x-forms.input label="Restore Command" wire:model='mysqlRestoreCommand'
|
||||
helper="Default restores from mysqldump --all-databases backups. Modify if needed."></x-forms.input>
|
||||
@if ($restoreCommandText)
|
||||
<div class="text-xs text-neutral-500">{{ $restoreCommandText }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@elseif ($resourceDbType === 'standalone-mariadb')
|
||||
@if ($dumpAll)
|
||||
<x-forms.textarea rows="14" readonly label="Custom Import Command"
|
||||
wire:model='restoreCommandText'></x-forms.textarea>
|
||||
@else
|
||||
<x-forms.input label="Custom Import Command" wire:model='mariadbRestoreCommand'></x-forms.input>
|
||||
@endif
|
||||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll'></x-forms.checkbox>
|
||||
<div class="flex flex-col gap-2 pt-4">
|
||||
<h3>Import Options</h3>
|
||||
<div class="flex flex-col gap-1 w-96">
|
||||
<x-forms.checkbox label="Legacy Backup all databases Backups" wire:model.live='legacyDumpAll'
|
||||
helper="For legacy backups created with the 'Backup all databases' option."></x-forms.checkbox>
|
||||
<x-forms.checkbox label="Legacy Single DB Backups" wire:model.live='legacySingleDb'
|
||||
helper="Uses mariadb for single database restores for backups with the format .sql."></x-forms.checkbox>
|
||||
</div>
|
||||
<x-forms.input label="Restore Command" wire:model='mariadbRestoreCommand'
|
||||
helper="Default restores from mariadb-dump --all-databases backups. Modify if needed."></x-forms.input>
|
||||
@if ($restoreCommandText)
|
||||
<div class="text-xs text-neutral-500">{{ $restoreCommandText }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
|
|
|||
|
|
@ -101,20 +101,17 @@ test('app:init marks stuck database backup executions as failed', function () {
|
|||
$runningBackup1 = ScheduledDatabaseBackupExecution::create([
|
||||
'scheduled_database_backup_id' => $scheduledBackup->id,
|
||||
'status' => 'running',
|
||||
'database_name' => 'test_db',
|
||||
]);
|
||||
|
||||
$runningBackup2 = ScheduledDatabaseBackupExecution::create([
|
||||
'scheduled_database_backup_id' => $scheduledBackup->id,
|
||||
'status' => 'running',
|
||||
'database_name' => 'test_db_2',
|
||||
]);
|
||||
|
||||
// Create a successful backup (should not be affected)
|
||||
$successfulBackup = ScheduledDatabaseBackupExecution::create([
|
||||
'scheduled_database_backup_id' => $scheduledBackup->id,
|
||||
'status' => 'success',
|
||||
'database_name' => 'test_db_3',
|
||||
'finished_at' => Carbon::now()->subMinutes(20),
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Project\Database\Import;
|
||||
|
||||
test('buildRestoreCommand handles PostgreSQL without dumpAll', function () {
|
||||
$component = new Import;
|
||||
$component->dumpAll = false;
|
||||
$component->postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB';
|
||||
|
||||
$database = Mockery::mock('App\Models\StandalonePostgresql');
|
||||
$database->shouldReceive('getMorphClass')->andReturn('App\Models\StandalonePostgresql');
|
||||
$component->resource = $database;
|
||||
|
||||
$result = $component->buildRestoreCommand('/tmp/test.dump');
|
||||
|
||||
expect($result)->toContain('pg_restore');
|
||||
expect($result)->toContain('/tmp/test.dump');
|
||||
});
|
||||
|
||||
test('buildRestoreCommand handles PostgreSQL with dumpAll', function () {
|
||||
$component = new Import;
|
||||
$component->dumpAll = true;
|
||||
// This is the full dump-all command prefix that would be set in the updatedDumpAll method
|
||||
$component->postgresqlRestoreCommand = 'psql -U $POSTGRES_USER -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname IS NOT NULL AND pid <> pg_backend_pid()" && psql -U $POSTGRES_USER -t -c "SELECT datname FROM pg_database WHERE NOT datistemplate" | xargs -I {} dropdb -U $POSTGRES_USER --if-exists {} && createdb -U $POSTGRES_USER postgres';
|
||||
|
||||
$database = Mockery::mock('App\Models\StandalonePostgresql');
|
||||
$database->shouldReceive('getMorphClass')->andReturn('App\Models\StandalonePostgresql');
|
||||
$component->resource = $database;
|
||||
|
||||
$result = $component->buildRestoreCommand('/tmp/test.dump');
|
||||
|
||||
expect($result)->toContain('gunzip -cf /tmp/test.dump');
|
||||
expect($result)->toContain('psql -U $POSTGRES_USER postgres');
|
||||
});
|
||||
|
||||
test('buildRestoreCommand handles MySQL without dumpAll', function () {
|
||||
$component = new Import;
|
||||
$component->dumpAll = false;
|
||||
$component->mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
|
||||
|
||||
$database = Mockery::mock('App\Models\StandaloneMysql');
|
||||
$database->shouldReceive('getMorphClass')->andReturn('App\Models\StandaloneMysql');
|
||||
$component->resource = $database;
|
||||
|
||||
$result = $component->buildRestoreCommand('/tmp/test.dump');
|
||||
|
||||
expect($result)->toContain('mysql -u $MYSQL_USER');
|
||||
expect($result)->toContain('< /tmp/test.dump');
|
||||
});
|
||||
|
||||
test('buildRestoreCommand handles MariaDB without dumpAll', function () {
|
||||
$component = new Import;
|
||||
$component->dumpAll = false;
|
||||
$component->mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
|
||||
|
||||
$database = Mockery::mock('App\Models\StandaloneMariadb');
|
||||
$database->shouldReceive('getMorphClass')->andReturn('App\Models\StandaloneMariadb');
|
||||
$component->resource = $database;
|
||||
|
||||
$result = $component->buildRestoreCommand('/tmp/test.dump');
|
||||
|
||||
expect($result)->toContain('mariadb -u $MARIADB_USER');
|
||||
expect($result)->toContain('< /tmp/test.dump');
|
||||
});
|
||||
|
||||
test('buildRestoreCommand handles MongoDB', function () {
|
||||
$component = new Import;
|
||||
$component->dumpAll = false;
|
||||
$component->mongodbRestoreCommand = 'mongorestore --authenticationDatabase=admin --username $MONGO_INITDB_ROOT_USERNAME --password $MONGO_INITDB_ROOT_PASSWORD --uri mongodb://localhost:27017 --gzip --archive=';
|
||||
|
||||
$database = Mockery::mock('App\Models\StandaloneMongodb');
|
||||
$database->shouldReceive('getMorphClass')->andReturn('App\Models\StandaloneMongodb');
|
||||
$component->resource = $database;
|
||||
|
||||
$result = $component->buildRestoreCommand('/tmp/test.dump');
|
||||
|
||||
expect($result)->toContain('mongorestore');
|
||||
expect($result)->toContain('/tmp/test.dump');
|
||||
});
|
||||
Loading…
Reference in a new issue