Added database import feature.
This commit is contained in:
parent
d04513d817
commit
557e1407d0
129
app/Livewire/Project/Database/Import.php
Normal file
129
app/Livewire/Project/Database/Import.php
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Project\Database;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\WithFileUploads;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\StandaloneMariadb;
|
||||||
|
use App\Models\StandaloneMongodb;
|
||||||
|
use App\Models\StandaloneMysql;
|
||||||
|
use App\Models\StandalonePostgresql;
|
||||||
|
use App\Models\StandaloneRedis;
|
||||||
|
|
||||||
|
class Import extends Component
|
||||||
|
{
|
||||||
|
use WithFileUploads;
|
||||||
|
|
||||||
|
public $file;
|
||||||
|
public $resource;
|
||||||
|
public $parameters;
|
||||||
|
public bool $validated = true;
|
||||||
|
public bool $scpInProgress = false;
|
||||||
|
public bool $importRunning = false;
|
||||||
|
public string $validationMsg = '';
|
||||||
|
public Server $server;
|
||||||
|
public string $container;
|
||||||
|
public array $importCommands = [];
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->parameters = get_route_parameters();
|
||||||
|
$this->getContainers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContainers()
|
||||||
|
{
|
||||||
|
$this->containers = collect();
|
||||||
|
if (!data_get($this->parameters, 'database_uuid')) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
|
||||||
|
if (is_null($resource)) {
|
||||||
|
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
|
||||||
|
if (is_null($resource)) {
|
||||||
|
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
|
||||||
|
if (is_null($resource)) {
|
||||||
|
$resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
|
||||||
|
if (is_null($resource)) {
|
||||||
|
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
|
||||||
|
if (is_null($resource)) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->resource = $resource;
|
||||||
|
$this->server = $this->resource->destination->server;
|
||||||
|
$this->container = $this->resource->uuid;
|
||||||
|
if (str(data_get($this,'resource.status'))->startsWith('running')) {
|
||||||
|
$this->containers->push($this->container);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->containers->count() > 1) {
|
||||||
|
$this->validated = false;
|
||||||
|
$this->validationMsg = 'The database service has more than one container running. Cannot import.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->resource->getMorphClass() == 'App\Models\StandaloneRedis'
|
||||||
|
|| $this->resource->getMorphClass() == 'App\Models\StandaloneMongodb') {
|
||||||
|
$this->validated = false;
|
||||||
|
$this->validationMsg = 'This database type is not currently supported.';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function runImport()
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'file' => 'required|file|max:102400'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importRunning = true;
|
||||||
|
$this->scpInProgress = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$uploadedFilename = $this->file->store('backup-import');
|
||||||
|
$path = \Storage::path($uploadedFilename);
|
||||||
|
$tmpPath = '/tmp/' . basename($uploadedFilename);
|
||||||
|
|
||||||
|
// SCP the backup file to the server.
|
||||||
|
instant_scp($path, $tmpPath, $this->server);
|
||||||
|
$this->scpInProgress = false;
|
||||||
|
|
||||||
|
$this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}";
|
||||||
|
|
||||||
|
switch ($this->resource->getMorphClass()) {
|
||||||
|
case 'App\Models\StandaloneMariadb':
|
||||||
|
$this->importCommands[] = "docker exec {$this->container} sh -c 'mariadb -u\$MARIADB_USER -p\$MARIADB_PASSWORD \$MARIADB_DATABASE < {$tmpPath}'";
|
||||||
|
$this->importCommands[] = "rm {$tmpPath}";
|
||||||
|
break;
|
||||||
|
case 'App\Models\StandaloneMysql':
|
||||||
|
$this->importCommands[] = "docker exec {$this->container} sh -c 'mysql -u\$MYSQL_USER -p\$MYSQL_PASSWORD \$MYSQL_DATABASE < {$tmpPath}'";
|
||||||
|
$this->importCommands[] = "rm {$tmpPath}";
|
||||||
|
break;
|
||||||
|
case 'App\Models\StandalonePostgresql':
|
||||||
|
$this->importCommands[] = "docker exec {$this->container} sh -c 'pg_restore -U \$POSTGRES_USER -d \$POSTGRES_DB {$tmpPath}'";
|
||||||
|
$this->importCommands[] = "rm {$tmpPath}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->importCommands[] = "docker exec {$this->container} sh -c 'rm {$tmpPath}'";
|
||||||
|
$this->importCommands[] = "docker exec {$this->container} sh -c 'echo \"Import finished with exit code $?\"'";
|
||||||
|
|
||||||
|
if (!empty($this->importCommands)) {
|
||||||
|
$activity = remote_process($this->importCommands, $this->server, ignore_errors: true);
|
||||||
|
$this->dispatch('newMonitorActivity', $activity->id);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->validated = false;
|
||||||
|
$this->validationMsg = $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -67,6 +67,47 @@ function savePrivateKeyToFs(Server $server)
|
|||||||
return $location;
|
return $location;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateScpCommand(Server $server, string $source, string $dest)
|
||||||
|
{
|
||||||
|
$user = $server->user;
|
||||||
|
$port = $server->port;
|
||||||
|
$privateKeyLocation = savePrivateKeyToFs($server);
|
||||||
|
$timeout = config('constants.ssh.command_timeout');
|
||||||
|
$connectionTimeout = config('constants.ssh.connection_timeout');
|
||||||
|
$serverInterval = config('constants.ssh.server_interval');
|
||||||
|
|
||||||
|
$scp_command = "timeout $timeout scp ";
|
||||||
|
$scp_command .= "-i {$privateKeyLocation} "
|
||||||
|
. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
|
||||||
|
. '-o PasswordAuthentication=no '
|
||||||
|
. "-o ConnectTimeout=$connectionTimeout "
|
||||||
|
. "-o ServerAliveInterval=$serverInterval "
|
||||||
|
. '-o RequestTTY=no '
|
||||||
|
. '-o LogLevel=ERROR '
|
||||||
|
. "-P {$port} "
|
||||||
|
. "{$source} "
|
||||||
|
. "{$user}@{$server->ip}:{$dest}";
|
||||||
|
|
||||||
|
return $scp_command;
|
||||||
|
}
|
||||||
|
function instant_scp(string $source, string $dest, Server $server, $throwError = true)
|
||||||
|
{
|
||||||
|
$timeout = config('constants.ssh.command_timeout');
|
||||||
|
$scp_command = generateScpCommand($server, $source, $dest);
|
||||||
|
$process = Process::timeout($timeout)->run($scp_command);
|
||||||
|
$output = trim($process->output());
|
||||||
|
$exitCode = $process->exitCode();
|
||||||
|
if ($exitCode !== 0) {
|
||||||
|
if (!$throwError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return excludeCertainErrors($process->errorOutput(), $exitCode);
|
||||||
|
}
|
||||||
|
if ($output === 'null') {
|
||||||
|
$output = null;
|
||||||
|
}
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
function generateSshCommand(Server $server, string $command, bool $isMux = true)
|
function generateSshCommand(Server $server, string $command, bool $isMux = true)
|
||||||
{
|
{
|
||||||
$user = $server->user;
|
$user = $server->user;
|
||||||
|
@ -53,7 +53,9 @@
|
|||||||
|
|
||||||
'temporary_file_upload' => [
|
'temporary_file_upload' => [
|
||||||
'disk' => null, // Example: 'local', 's3' | Default: 'default'
|
'disk' => null, // Example: 'local', 's3' | Default: 'default'
|
||||||
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
|
'rules' => [ // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
|
||||||
|
'file', 'max:256000'
|
||||||
|
],
|
||||||
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
|
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
|
||||||
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
|
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
|
||||||
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
|
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
|
||||||
|
@ -37,3 +37,7 @@ RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
|
|||||||
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
|
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
|
||||||
;fi"
|
;fi"
|
||||||
|
|
||||||
|
RUN { \
|
||||||
|
echo 'upload_max_filesize=256M'; \
|
||||||
|
echo 'post_max_size=256M'; \
|
||||||
|
} > /etc/php/current_version/cli/conf.d/upload-limits.ini
|
@ -62,3 +62,8 @@ RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
|
|||||||
echo 'arm64' && \
|
echo 'arm64' && \
|
||||||
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
|
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
|
||||||
;fi"
|
;fi"
|
||||||
|
|
||||||
|
RUN { \
|
||||||
|
echo 'upload_max_filesize=256M'; \
|
||||||
|
echo 'post_max_size=256M'; \
|
||||||
|
} > /etc/php/current_version/cli/conf.d/upload-limits.ini
|
41
resources/views/livewire/project/database/import.blade.php
Normal file
41
resources/views/livewire/project/database/import.blade.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<div>
|
||||||
|
<div class="mb-10 rounded alert alert-warning">
|
||||||
|
<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"
|
||||||
|
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>
|
||||||
|
<span>This is a destructive action, existing data will be replaced!</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!$validated)
|
||||||
|
<div>{{ $validationMsg }}</div>
|
||||||
|
@else
|
||||||
|
@if (!$importRunning)
|
||||||
|
<form disabled wire:submit.prevent="runImport">
|
||||||
|
<div class="flex items-end gap-2"
|
||||||
|
x-data="{ isFinished: false, isUploading: false, progress: 0 }"
|
||||||
|
x-on:livewire-upload-start="isUploading = true; isFinished = false"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<input type="file" id="file" wire:model="file">
|
||||||
|
@error('file') <span class="error">{{ $message }}</span> @enderror
|
||||||
|
<x-forms.button type="submit" x-show="isFinished">Import</x-forms.button>
|
||||||
|
<div x-show="isUploading">
|
||||||
|
<progress max="100" x-bind:value="progress"></progress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($scpInProgress)
|
||||||
|
<div>Database backup is being copied to server..</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="container w-full pt-10 mx-auto">
|
||||||
|
<livewire:activity-monitor header="Database import output" />
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -39,6 +39,11 @@
|
|||||||
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'"
|
||||||
@ -74,6 +79,9 @@
|
|||||||
<div x-cloak x-show="activeTab === 'resource-limits'">
|
<div x-cloak x-show="activeTab === 'resource-limits'">
|
||||||
<livewire:project.shared.resource-limits :resource="$database" />
|
<livewire:project.shared.resource-limits :resource="$database" />
|
||||||
</div>
|
</div>
|
||||||
|
<div x-cloak x-show="activeTab === 'import'">
|
||||||
|
<livewire:project.database.import :resource="$database" />
|
||||||
|
</div>
|
||||||
<div x-cloak x-show="activeTab === 'danger'">
|
<div x-cloak x-show="activeTab === 'danger'">
|
||||||
<livewire:project.shared.danger :resource="$database" />
|
<livewire:project.shared.danger :resource="$database" />
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user