feat: import backups
This commit is contained in:
parent
6bb45430c9
commit
6b24001876
@ -11,6 +11,7 @@ use App\Models\StandaloneMongodb;
|
|||||||
use App\Models\StandaloneMysql;
|
use App\Models\StandaloneMysql;
|
||||||
use App\Models\StandalonePostgresql;
|
use App\Models\StandalonePostgresql;
|
||||||
use App\Models\StandaloneRedis;
|
use App\Models\StandaloneRedis;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class Import extends Component
|
class Import extends Component
|
||||||
{
|
{
|
||||||
@ -19,6 +20,7 @@ class Import extends Component
|
|||||||
public $file;
|
public $file;
|
||||||
public $resource;
|
public $resource;
|
||||||
public $parameters;
|
public $parameters;
|
||||||
|
public $containers;
|
||||||
public bool $validated = true;
|
public bool $validated = true;
|
||||||
public bool $scpInProgress = false;
|
public bool $scpInProgress = false;
|
||||||
public bool $importRunning = false;
|
public bool $importRunning = false;
|
||||||
@ -26,7 +28,17 @@ class Import extends Component
|
|||||||
public Server $server;
|
public Server $server;
|
||||||
public string $container;
|
public string $container;
|
||||||
public array $importCommands = [];
|
public array $importCommands = [];
|
||||||
|
public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB';
|
||||||
|
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p $MYSQL_PASSWORD $MYSQL_DATABASE';
|
||||||
|
public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p $MARIADB_PASSWORD $MARIADB_DATABASE';
|
||||||
|
|
||||||
|
public function getListeners()
|
||||||
|
{
|
||||||
|
$userId = auth()->user()->id;
|
||||||
|
return [
|
||||||
|
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||||
|
];
|
||||||
|
}
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
$this->parameters = get_route_parameters();
|
$this->parameters = get_route_parameters();
|
||||||
@ -59,7 +71,7 @@ class Import extends Component
|
|||||||
$this->resource = $resource;
|
$this->resource = $resource;
|
||||||
$this->server = $this->resource->destination->server;
|
$this->server = $this->resource->destination->server;
|
||||||
$this->container = $this->resource->uuid;
|
$this->container = $this->resource->uuid;
|
||||||
if (str(data_get($this,'resource.status'))->startsWith('running')) {
|
if (str(data_get($this, 'resource.status'))->startsWith('running')) {
|
||||||
$this->containers->push($this->container);
|
$this->containers->push($this->container);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,14 +80,15 @@ class Import extends Component
|
|||||||
$this->validationMsg = 'The database service has more than one container running. Cannot import.';
|
$this->validationMsg = 'The database service has more than one container running. Cannot import.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->resource->getMorphClass() == 'App\Models\StandaloneRedis'
|
if (
|
||||||
|| $this->resource->getMorphClass() == 'App\Models\StandaloneMongodb') {
|
$this->resource->getMorphClass() == 'App\Models\StandaloneRedis'
|
||||||
|
|| $this->resource->getMorphClass() == 'App\Models\StandaloneMongodb'
|
||||||
|
) {
|
||||||
$this->validated = false;
|
$this->validated = false;
|
||||||
$this->validationMsg = 'This database type is not currently supported.';
|
$this->validationMsg = 'This database type is not currently supported.';
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function runImport()
|
public function runImport()
|
||||||
{
|
{
|
||||||
$this->validate([
|
$this->validate([
|
||||||
@ -87,7 +100,7 @@ class Import extends Component
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$uploadedFilename = $this->file->store('backup-import');
|
$uploadedFilename = $this->file->store('backup-import');
|
||||||
$path = \Storage::path($uploadedFilename);
|
$path = Storage::path($uploadedFilename);
|
||||||
$tmpPath = '/tmp/' . basename($uploadedFilename);
|
$tmpPath = '/tmp/' . basename($uploadedFilename);
|
||||||
|
|
||||||
// SCP the backup file to the server.
|
// SCP the backup file to the server.
|
||||||
@ -98,17 +111,17 @@ class Import extends Component
|
|||||||
|
|
||||||
switch ($this->resource->getMorphClass()) {
|
switch ($this->resource->getMorphClass()) {
|
||||||
case 'App\Models\StandaloneMariadb':
|
case 'App\Models\StandaloneMariadb':
|
||||||
$this->importCommands[] = "docker exec {$this->container} sh -c 'mariadb -u\$MARIADB_USER -p\$MARIADB_PASSWORD \$MARIADB_DATABASE < {$tmpPath}'";
|
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mariadbRestoreCommand} < {$tmpPath}'";
|
||||||
$this->importCommands[] = "rm {$tmpPath}";
|
$this->importCommands[] = "rm {$tmpPath}";
|
||||||
break;
|
break;
|
||||||
case 'App\Models\StandaloneMysql':
|
case 'App\Models\StandaloneMysql':
|
||||||
$this->importCommands[] = "docker exec {$this->container} sh -c 'mysql -u\$MYSQL_USER -p\$MYSQL_PASSWORD \$MYSQL_DATABASE < {$tmpPath}'";
|
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mysqlRestoreCommand} < {$tmpPath}'";
|
||||||
$this->importCommands[] = "rm {$tmpPath}";
|
$this->importCommands[] = "rm {$tmpPath}";
|
||||||
break;
|
break;
|
||||||
case 'App\Models\StandalonePostgresql':
|
case 'App\Models\StandalonePostgresql':
|
||||||
$this->importCommands[] = "docker exec {$this->container} sh -c 'pg_restore -U \$POSTGRES_USER -d \$POSTGRES_DB {$tmpPath}'";
|
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->postgresqlRestoreCommand} {$tmpPath}'";
|
||||||
$this->importCommands[] = "rm {$tmpPath}";
|
$this->importCommands[] = "rm {$tmpPath}";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->importCommands[] = "docker exec {$this->container} sh -c 'rm {$tmpPath}'";
|
$this->importCommands[] = "docker exec {$this->container} sh -c 'rm {$tmpPath}'";
|
||||||
@ -122,8 +135,5 @@ class Import extends Component
|
|||||||
$this->validated = false;
|
$this->validated = false;
|
||||||
$this->validationMsg = $e->getMessage();
|
$this->validationMsg = $e->getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,11 @@
|
|||||||
window.location.hash = 'storages'"
|
window.location.hash = 'storages'"
|
||||||
href="#">Storages
|
href="#">Storages
|
||||||
</a>
|
</a>
|
||||||
|
<a :class="activeTab === 'import' && 'text-white'"
|
||||||
|
@click.prevent="activeTab = 'import';
|
||||||
|
window.location.hash = 'import'" href="#">Import
|
||||||
|
Backup
|
||||||
|
</a>
|
||||||
<a :class="activeTab === 'webhooks' && 'text-white'"
|
<a :class="activeTab === 'webhooks' && 'text-white'"
|
||||||
@click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks
|
@click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks
|
||||||
</a>
|
</a>
|
||||||
@ -39,11 +44,7 @@
|
|||||||
window.location.hash = 'resource-limits'"
|
window.location.hash = 'resource-limits'"
|
||||||
href="#">Resource Limits
|
href="#">Resource Limits
|
||||||
</a>
|
</a>
|
||||||
<a :class="activeTab === 'import' && 'text-white'"
|
|
||||||
@click.prevent="activeTab = 'import';
|
|
||||||
window.location.hash = 'import'"
|
|
||||||
href="#">Import
|
|
||||||
</a>
|
|
||||||
<a :class="activeTab === 'danger' && 'text-white'"
|
<a :class="activeTab === 'danger' && 'text-white'"
|
||||||
@click.prevent="activeTab = 'danger';
|
@click.prevent="activeTab = 'danger';
|
||||||
window.location.hash = 'danger'"
|
window.location.hash = 'danger'"
|
||||||
|
@ -1,41 +1,60 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="mb-10 rounded alert alert-warning">
|
<h2>Import Backup</h2>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
|
<div class="mt-2 mb-4 rounded alert alert-error">
|
||||||
viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>This is a destructive action, existing data will be replaced!</span>
|
<span>This is a destructive action, existing data will be replaced!</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!$validated)
|
@if (str(data_get($resource, 'status'))->startsWith('running'))
|
||||||
<div>{{ $validationMsg }}</div>
|
@if (!$validated)
|
||||||
@else
|
<div>{{ $validationMsg }}</div>
|
||||||
@if (!$importRunning)
|
@else
|
||||||
<form disabled wire:submit.prevent="runImport">
|
<form disabled wire:submit.prevent="runImport" x-data="{ isFinished: false, isUploading: false, progress: 0 }">
|
||||||
<div class="flex items-end gap-2"
|
@if ($resource->type() === 'standalone-postgresql')
|
||||||
x-data="{ isFinished: false, isUploading: false, progress: 0 }"
|
<x-forms.input class="mb-2" label="Custom Import Command"
|
||||||
x-on:livewire-upload-start="isUploading = true; isFinished = false"
|
wire:model='postgresqlRestoreCommand'></x-forms.input>
|
||||||
x-on:livewire-upload-finish="isUploading = false; isFinished = true"
|
@elseif ($resource->type() === 'standalone-mysql')
|
||||||
x-on:livewire-upload-error="isUploading = false"
|
<x-forms.input class="mb-2" label="Custom Import Command"
|
||||||
x-on:livewire-upload-progress="progress = $event.detail.progress"
|
wire:model='mysqlRestoreCommand'></x-forms.input>
|
||||||
>
|
@elseif ($resource->type() === 'standalone-mariadb')
|
||||||
<input type="file" id="file" wire:model="file">
|
<x-forms.input class="mb-2" label="Custom Import Command"
|
||||||
@error('file') <span class="error">{{ $message }}</span> @enderror
|
wire:model='mariadbRestoreCommand'></x-forms.input>
|
||||||
<x-forms.button type="submit" x-show="isFinished">Import</x-forms.button>
|
@endif
|
||||||
<div x-show="isUploading">
|
<div x-on:livewire-upload-start="isUploading = true; isFinished = false"
|
||||||
<progress max="100" x-bind:value="progress"></progress>
|
x-on:livewire-upload-finish="isUploading = false; isFinished = true"
|
||||||
|
x-on:livewire-upload-error="isUploading = false"
|
||||||
|
x-on:livewire-upload-progress="progress = $event.detail.progress">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="file_input">Upload
|
||||||
|
file</label>
|
||||||
|
<input wire:model="file"
|
||||||
|
class="block w-full text-sm rounded cursor-pointer text-whiteborder bg-coolgray-100 border-coolgray-400 focus:outline-none"
|
||||||
|
aria-describedby="file_input_help" id="file_input" type="file">
|
||||||
|
<p class="mt-1 text-sm text-neutral-500" id="file_input_help">Max file size: 100MB
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@error('file')
|
||||||
|
<span class="error">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
<div x-show="isUploading">
|
||||||
|
<progress max="100" x-bind:value="progress"
|
||||||
|
class="progress progress-warning"></progress>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<x-forms.button type="submit" class="w-full mt-4" x-show="isFinished">Import Backup</x-forms.button>
|
||||||
</form>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
@endif
|
|
||||||
|
|
||||||
@if ($scpInProgress)
|
@if ($scpInProgress)
|
||||||
<div>Database backup is being copied to server..</div>
|
<div>Database backup is being copied to server...</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="container w-full pt-10 mx-auto">
|
<div class="container w-full pt-4 mx-auto">
|
||||||
<livewire:activity-monitor header="Database import output" />
|
<livewire:activity-monitor header="Database import output" />
|
||||||
</div>
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="text-neutral-500">Database must be running to import a backup.</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<x-forms.input placeholder="Run cron" id="name" label="Name" required />
|
<x-forms.input placeholder="Run cron" id="name" label="Name" required />
|
||||||
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" required />
|
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" required />
|
||||||
<x-forms.input placeholder="0 0 * * * or daily" id="frequency" label="Frequency" required />
|
<x-forms.input placeholder="0 0 * * * or daily" id="frequency" label="Frequency" required />
|
||||||
<x-forms.input placeholder="php" id="container" label="Container name" />
|
<x-forms.input placeholder="php" id="container" helper="You can leave it empty if your resource only have one container." label="Container name" />
|
||||||
<x-forms.button onclick="newTask.close()" type="submit">
|
<x-forms.button onclick="newTask.close()" type="submit">
|
||||||
Save
|
Save
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
@forelse($resource->scheduled_tasks as $task)
|
@forelse($resource->scheduled_tasks as $task)
|
||||||
<a class="flex flex-col box"
|
<a class="flex flex-col box"
|
||||||
|
|
||||||
@if ($resource->type() == 'application')
|
@if ($resource->type() == 'application')
|
||||||
href="{{ route('project.application.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
|
href="{{ route('project.application.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
|
||||||
@elseif ($resource->type() == 'service')
|
@elseif ($resource->type() == 'service')
|
||||||
@ -19,7 +18,7 @@
|
|||||||
<div>Last run: {{ data_get($task->latest_log, 'status', 'No runs yet') }}</div>
|
<div>Last run: {{ data_get($task->latest_log, 'status', 'No runs yet') }}</div>
|
||||||
</a>
|
</a>
|
||||||
@empty
|
@empty
|
||||||
<div>No scheduled tasks configured.</div>
|
<div class="text-neutral-500">No scheduled tasks configured.</div>
|
||||||
@endforelse
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user