fix: backup database one-by-one.
This commit is contained in:
parent
49c56524e1
commit
d635e5dbae
@ -17,6 +17,7 @@ class BackupEdit extends Component
|
|||||||
'backup.number_of_backups_locally' => 'required|integer|min:1',
|
'backup.number_of_backups_locally' => 'required|integer|min:1',
|
||||||
'backup.save_s3' => 'required|boolean',
|
'backup.save_s3' => 'required|boolean',
|
||||||
'backup.s3_storage_id' => 'nullable|integer',
|
'backup.s3_storage_id' => 'nullable|integer',
|
||||||
|
'backup.databases_to_backup' => 'nullable',
|
||||||
];
|
];
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
'backup.enabled' => 'Enabled',
|
'backup.enabled' => 'Enabled',
|
||||||
@ -24,6 +25,7 @@ class BackupEdit extends Component
|
|||||||
'backup.number_of_backups_locally' => 'Number of Backups Locally',
|
'backup.number_of_backups_locally' => 'Number of Backups Locally',
|
||||||
'backup.save_s3' => 'Save to S3',
|
'backup.save_s3' => 'Save to S3',
|
||||||
'backup.s3_storage_id' => 'S3 Storage',
|
'backup.s3_storage_id' => 'S3 Storage',
|
||||||
|
'backup.databases_to_backup' => 'Databases to Backup',
|
||||||
];
|
];
|
||||||
protected $messages = [
|
protected $messages = [
|
||||||
'backup.s3_storage_id' => 'Select a S3 Storage',
|
'backup.s3_storage_id' => 'Select a S3 Storage',
|
||||||
@ -37,7 +39,6 @@ class BackupEdit extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function delete()
|
public function delete()
|
||||||
{
|
{
|
||||||
// TODO: Delete backup from server and add a confirmation modal
|
// TODO: Delete backup from server and add a confirmation modal
|
||||||
@ -49,6 +50,7 @@ class BackupEdit extends Component
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->custom_validate();
|
$this->custom_validate();
|
||||||
|
|
||||||
$this->backup->save();
|
$this->backup->save();
|
||||||
$this->backup->refresh();
|
$this->backup->refresh();
|
||||||
$this->emit('success', 'Backup updated successfully');
|
$this->emit('success', 'Backup updated successfully');
|
||||||
@ -71,9 +73,11 @@ class BackupEdit extends Component
|
|||||||
|
|
||||||
public function submit()
|
public function submit()
|
||||||
{
|
{
|
||||||
ray($this->backup->s3_storage_id);
|
|
||||||
try {
|
try {
|
||||||
$this->custom_validate();
|
$this->custom_validate();
|
||||||
|
if ($this->backup->databases_to_backup == '' || $this->backup->databases_to_backup === null) {
|
||||||
|
$this->backup->databases_to_backup = null;
|
||||||
|
}
|
||||||
$this->backup->save();
|
$this->backup->save();
|
||||||
$this->backup->refresh();
|
$this->backup->refresh();
|
||||||
$this->emit('success', 'Backup updated successfully');
|
$this->emit('success', 'Backup updated successfully');
|
||||||
|
@ -13,6 +13,6 @@ class BackupNow extends Component
|
|||||||
dispatch(new DatabaseBackupJob(
|
dispatch(new DatabaseBackupJob(
|
||||||
backup: $this->backup
|
backup: $this->backup
|
||||||
));
|
));
|
||||||
$this->emit('success', 'Backup queued. It will be available in a few minutes');
|
$this->emit('success', 'Backup queued. It will be available in a few minutes.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,10 +78,10 @@ class Backup extends Component
|
|||||||
dispatch(new DatabaseBackupJob(
|
dispatch(new DatabaseBackupJob(
|
||||||
backup: $this->backup
|
backup: $this->backup
|
||||||
));
|
));
|
||||||
$this->emit('success', 'Backup queued. It will be available in a few minutes');
|
$this->emit('success', 'Backup queued. It will be available in a few minutes.');
|
||||||
}
|
}
|
||||||
public function submit()
|
public function submit()
|
||||||
{
|
{
|
||||||
$this->emit('success', 'Backup updated successfully');
|
$this->emit('success', 'Backup updated successfully.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,50 +66,77 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
|
|||||||
ray('database not running');
|
ray('database not running');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
$databaseType = $this->database->type();
|
||||||
|
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
|
||||||
|
|
||||||
|
if (is_null($databasesToBackup)) {
|
||||||
|
if ($databaseType === 'standalone-postgresql') {
|
||||||
|
$databasesToBackup = [$this->database->postgres_db];
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$databasesToBackup = explode(',', $databasesToBackup);
|
||||||
|
$databasesToBackup = array_map('trim', $databasesToBackup);
|
||||||
|
}
|
||||||
$this->container_name = $this->database->uuid;
|
$this->container_name = $this->database->uuid;
|
||||||
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name;
|
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name;
|
||||||
|
|
||||||
if ($this->database->name === 'coolify-db') {
|
if ($this->database->name === 'coolify-db') {
|
||||||
|
$databasesToBackup = ['coolify'];
|
||||||
$this->container_name = "coolify-db";
|
$this->container_name = "coolify-db";
|
||||||
$ip = Str::slug($this->server->ip);
|
$ip = Str::slug($this->server->ip);
|
||||||
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
|
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
|
||||||
}
|
}
|
||||||
$this->backup_file = "/pg-backup-customformat-" . Carbon::now()->timestamp . ".backup";
|
foreach ($databasesToBackup as $database) {
|
||||||
$this->backup_location = $this->backup_dir . $this->backup_file;
|
$size = 0;
|
||||||
|
ray('Backing up ' . $database);
|
||||||
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
try {
|
||||||
'filename' => $this->backup_location,
|
$this->backup_file = "/pg-dump-$database-" . Carbon::now()->timestamp . ".dmp";
|
||||||
'scheduled_database_backup_id' => $this->backup->id,
|
$this->backup_location = $this->backup_dir . $this->backup_file;
|
||||||
]);
|
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
||||||
if ($this->database->type() === 'standalone-postgresql') {
|
'database_name' => $database,
|
||||||
$this->backup_standalone_postgresql();
|
'filename' => $this->backup_location,
|
||||||
|
'scheduled_database_backup_id' => $this->backup->id,
|
||||||
|
]);
|
||||||
|
if ($databaseType === 'standalone-postgresql') {
|
||||||
|
$this->backup_standalone_postgresql($database);
|
||||||
|
}
|
||||||
|
$size = $this->calculate_size();
|
||||||
|
$this->remove_old_backups();
|
||||||
|
if ($this->backup->save_s3) {
|
||||||
|
$this->upload_to_s3();
|
||||||
|
}
|
||||||
|
$this->team->notify(new BackupSuccess($this->backup, $this->database));
|
||||||
|
$this->backup_log->update([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => $this->backup_output,
|
||||||
|
'size' => $size,
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->backup_log->update([
|
||||||
|
'status' => 'failed',
|
||||||
|
'message' => $this->backup_output,
|
||||||
|
'size' => $size,
|
||||||
|
'filename' => null
|
||||||
|
]);
|
||||||
|
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
|
||||||
|
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$this->calculate_size();
|
|
||||||
$this->remove_old_backups();
|
|
||||||
if ($this->backup->save_s3) {
|
|
||||||
$this->upload_to_s3();
|
|
||||||
}
|
|
||||||
$this->save_backup_logs();
|
|
||||||
$this->team->notify(new BackupSuccess($this->backup, $this->database));
|
|
||||||
$this->backup_status = 'success';
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->backup_status = 'failed';
|
|
||||||
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
|
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
|
||||||
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
|
|
||||||
throw $e;
|
throw $e;
|
||||||
} finally {
|
|
||||||
$this->backup_log->update([
|
|
||||||
'status' => $this->backup_status,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function backup_standalone_postgresql(): void
|
private function backup_standalone_postgresql(string $database): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
ray($this->backup_dir);
|
ray($this->backup_dir);
|
||||||
$commands[] = "mkdir -p " . $this->backup_dir;
|
$commands[] = "mkdir -p " . $this->backup_dir;
|
||||||
$commands[] = "docker exec $this->container_name pg_dump -Fc -U {$this->database->postgres_user} > $this->backup_location";
|
$commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
|
||||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||||
$this->backup_output = trim($this->backup_output);
|
$this->backup_output = trim($this->backup_output);
|
||||||
if ($this->backup_output === '') {
|
if ($this->backup_output === '') {
|
||||||
@ -119,6 +146,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
|
|||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->add_to_backup_output($e->getMessage());
|
$this->add_to_backup_output($e->getMessage());
|
||||||
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
|
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
|
||||||
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,9 +159,9 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function calculate_size(): void
|
private function calculate_size()
|
||||||
{
|
{
|
||||||
$this->size = instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server);
|
return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function remove_old_backups(): void
|
private function remove_old_backups(): void
|
||||||
@ -180,13 +208,4 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
|
|||||||
instant_remote_process([$command], $this->server);
|
instant_remote_process([$command], $this->server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function save_backup_logs(): void
|
|
||||||
{
|
|
||||||
$this->backup_log->update([
|
|
||||||
'status' => $this->backup_status,
|
|
||||||
'message' => $this->backup_output,
|
|
||||||
'size' => $this->size,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('scheduled_database_backups', function (Blueprint $table) {
|
||||||
|
$table->text('databases_to_backup')->nullable();
|
||||||
|
});
|
||||||
|
Schema::table('scheduled_database_backup_executions', function (Blueprint $table) {
|
||||||
|
$table->string('database_name')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('scheduled_database_backups', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('databases_to_backup');
|
||||||
|
});
|
||||||
|
Schema::table('scheduled_database_backup_executions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('database_name');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -26,6 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
<x-forms.input label="Databases To Backup" helper="Comma separated list of databases to backup. Empty will include the default one." id="backup.databases_to_backup" />
|
||||||
<x-forms.input label="Frequency" id="backup.frequency" />
|
<x-forms.input label="Frequency" id="backup.frequency" />
|
||||||
<x-forms.input label="Number of backups to keep (locally)" id="backup.number_of_backups_locally" />
|
<x-forms.input label="Number of backups to keep (locally)" id="backup.number_of_backups_locally" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
<div class="flex flex-col flex-col-reverse gap-2">
|
<div class="flex flex-col-reverse gap-2">
|
||||||
@forelse($executions as $execution)
|
@forelse($executions as $execution)
|
||||||
<form class="flex flex-col p-2 border-dotted border-1 bg-coolgray-300" @class([
|
<form class="flex flex-col p-2 border-dotted border-1 bg-coolgray-300" @class([
|
||||||
'border-green-500' => data_get($execution, 'status') === 'success',
|
'border-green-500' => data_get($execution, 'status') === 'success',
|
||||||
'border-red-500' => data_get($execution, 'status') === 'failed',
|
'border-red-500' => data_get($execution, 'status') === 'failed',
|
||||||
])>
|
])>
|
||||||
<div>Started At: {{ data_get($execution, 'created_at') }}</div>
|
<div>Database: {{ data_get($execution, 'database_name', 'N/A') }}</div>
|
||||||
<div>Status: {{ data_get($execution, 'status') }}</div>
|
<div>Status: {{ data_get($execution, 'status') }}</div>
|
||||||
|
<div>Started At: {{ data_get($execution, 'created_at') }}</div>
|
||||||
@if (data_get($execution, 'message'))
|
@if (data_get($execution, 'message'))
|
||||||
<div>Message: {{ data_get($execution, 'message') }}</div>
|
<div>Message: {{ data_get($execution, 'message') }}</div>
|
||||||
@endif
|
@endif
|
||||||
<div>Size: {{ data_get($execution, 'size') }} B / {{ round((int) data_get($execution, 'size') / 1024, 2) }}
|
<div>Size: {{ data_get($execution, 'size') }} B / {{ round((int) data_get($execution, 'size') / 1024, 2) }}
|
||||||
kB / {{ round((int) data_get($execution, 'size') / 1024 / 1024, 2) }} MB
|
kB / {{ round((int) data_get($execution, 'size') / 1024 / 1024, 3) }} MB
|
||||||
</div>
|
</div>
|
||||||
<div>Location: {{ data_get($execution, 'filename') }}</div>
|
<div>Location: {{ data_get($execution, 'filename', 'N/A') }}</div>
|
||||||
<livewire:project.database.backup-execution :execution="$execution" :wire:key="$execution->id" />
|
<livewire:project.database.backup-execution :execution="$execution" :wire:key="$execution->id" />
|
||||||
</form>
|
</form>
|
||||||
@empty
|
@empty
|
||||||
|
Loading…
x
Reference in New Issue
Block a user