mirror of
https://github.com/coollabsio/coolify.git
synced 2026-03-11 08:55:47 +00:00
feat: Add terminal file upload functionality
This commit is contained in:
parent
7bbfa094d0
commit
72020a7363
7 changed files with 544 additions and 22 deletions
|
|
@ -5,6 +5,7 @@ namespace App\Http\Controllers;
|
|||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException;
|
||||
use Pion\Laravel\ChunkUpload\Handler\HandlerFactory;
|
||||
use Pion\Laravel\ChunkUpload\Receiver\FileReceiver;
|
||||
|
|
@ -20,7 +21,7 @@ class UploadController extends BaseController
|
|||
$receiver = new FileReceiver('file', $request, HandlerFactory::classFromRequest($request));
|
||||
|
||||
if ($receiver->isUploaded() === false) {
|
||||
throw new UploadMissingFileException;
|
||||
throw new UploadMissingFileException();
|
||||
}
|
||||
|
||||
$save = $receiver->receive();
|
||||
|
|
@ -69,13 +70,57 @@ class UploadController extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
protected function createFilename(UploadedFile $file)
|
||||
public function uploadTerminalFile(Request $request)
|
||||
{
|
||||
$extension = $file->getClientOriginalExtension();
|
||||
$filename = str_replace('.'.$extension, '', $file->getClientOriginalName()); // Filename without extension
|
||||
// Security: Verify user has permission to upload terminal files
|
||||
if (! Auth::check()) {
|
||||
return response()->json(['error' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$filename .= '_'.md5(time()).'.'.$extension;
|
||||
// Check if user is admin or has terminal access
|
||||
$user = Auth::user();
|
||||
if (! $user->isAdmin() && ! $user->isInstanceAdmin()) {
|
||||
return response()->json(['error' => 'You do not have permission to upload terminal files'], 403);
|
||||
}
|
||||
|
||||
return $filename;
|
||||
$receiver = new FileReceiver('file', $request, HandlerFactory::classFromRequest($request));
|
||||
|
||||
if ($receiver->isUploaded() === false) {
|
||||
throw new UploadMissingFileException();
|
||||
}
|
||||
|
||||
$save = $receiver->receive();
|
||||
|
||||
if ($save->isFinished()) {
|
||||
return $this->saveTerminalFile($save->getFile());
|
||||
}
|
||||
|
||||
$handler = $save->handler();
|
||||
|
||||
return response()->json([
|
||||
'done' => $handler->getPercentageDone(),
|
||||
'status' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function saveTerminalFile(UploadedFile $file)
|
||||
{
|
||||
$mime = str_replace('/', '-', $file->getMimeType());
|
||||
$filePath = 'terminal-uploads/temp';
|
||||
$finalPath = storage_path('app/'.$filePath);
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (! is_dir($finalPath)) {
|
||||
mkdir($finalPath, 0755, true);
|
||||
}
|
||||
|
||||
// Use original filename with timestamp to avoid conflicts
|
||||
$filename = time().'_'.$file->getClientOriginalName();
|
||||
$file->move($finalPath, $filename);
|
||||
|
||||
return response()->json([
|
||||
'mime_type' => $mime,
|
||||
'filename' => $filename,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
80
app/Jobs/CleanupExpiredTerminalFilesJob.php
Normal file
80
app/Jobs/CleanupExpiredTerminalFilesJob.php
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class CleanupExpiredTerminalFilesJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public $timeout = 300; // 5 minutes
|
||||
|
||||
public function __construct(
|
||||
public string $localPath,
|
||||
public string $serverPath,
|
||||
public int $serverId,
|
||||
public ?string $containerUuid,
|
||||
public string $filename
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
// Delete local file
|
||||
if (file_exists($this->localPath)) {
|
||||
unlink($this->localPath);
|
||||
Log::info("Cleaned up local terminal file: {$this->localPath}");
|
||||
}
|
||||
|
||||
// Delete file from server
|
||||
$server = Server::find($this->serverId);
|
||||
if ($server) {
|
||||
// Remove from server
|
||||
$result = instant_remote_process([
|
||||
"rm -f {$this->serverPath}"
|
||||
], $server, throwError: false);
|
||||
|
||||
if ($result) {
|
||||
Log::info("Cleaned up server terminal file: {$this->serverPath}");
|
||||
}
|
||||
|
||||
// If container was specified, remove from container as well
|
||||
if ($this->containerUuid) {
|
||||
$containerPath = "/tmp/{$this->filename}";
|
||||
instant_remote_process([
|
||||
"docker exec {$this->containerUuid} rm -f {$containerPath} 2>/dev/null || true"
|
||||
], $server, throwError: false);
|
||||
|
||||
Log::info("Cleaned up container terminal file: {$containerPath}");
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up empty parent directory
|
||||
$parentDir = dirname($this->localPath);
|
||||
if (is_dir($parentDir) && count(scandir($parentDir)) === 2) { // Only . and ..
|
||||
rmdir($parentDir);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("Failed to cleanup terminal file: {$e->getMessage()}", [
|
||||
'localPath' => $this->localPath,
|
||||
'serverPath' => $this->serverPath,
|
||||
'serverId' => $this->serverId,
|
||||
]);
|
||||
|
||||
// Don't fail the job, just log the error
|
||||
// Files will eventually be cleaned up by manual cleanup or system maintenance
|
||||
}
|
||||
}
|
||||
}
|
||||
180
app/Livewire/Terminal/FileImport.php
Normal file
180
app/Livewire/Terminal/FileImport.php
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Terminal;
|
||||
|
||||
use App\Jobs\CleanupExpiredTerminalFilesJob;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class FileImport extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use WithFileUploads;
|
||||
|
||||
public string $selectedUuid;
|
||||
|
||||
public ?string $selectedServerUuid = null;
|
||||
|
||||
public ?string $targetName = null;
|
||||
|
||||
public $uploadedFile;
|
||||
|
||||
public ?string $filename = null;
|
||||
|
||||
public ?string $filesize = null;
|
||||
|
||||
public ?string $filePath = null;
|
||||
|
||||
public bool $isUploading = false;
|
||||
|
||||
public int $progress = 0;
|
||||
|
||||
public bool $error = false;
|
||||
|
||||
public int $expirationMinutes = 60; // Default: 1 hour
|
||||
|
||||
public function updatedUploadedFile()
|
||||
{
|
||||
$this->validate([
|
||||
'uploadedFile' => 'required|file|max:10485760', // 10GB in KB
|
||||
]);
|
||||
|
||||
$this->filename = $this->uploadedFile->getClientOriginalName();
|
||||
$this->filesize = number_format($this->uploadedFile->getSize() / 1024 / 1024, 2) . ' MB';
|
||||
$this->isUploading = false;
|
||||
|
||||
$this->dispatch('success', 'File uploaded successfully!');
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function expirationOptions()
|
||||
{
|
||||
return [
|
||||
15 => '15 minutes',
|
||||
30 => '30 minutes',
|
||||
60 => '1 hour',
|
||||
120 => '2 hours',
|
||||
240 => '4 hours',
|
||||
480 => '8 hours',
|
||||
1440 => '24 hours',
|
||||
];
|
||||
}
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},FileUploadCompleted" => 'handleUploadCompleted',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount(string $selectedUuid, string $targetName, ?string $selectedServerUuid = null)
|
||||
{
|
||||
$this->selectedUuid = $selectedUuid;
|
||||
$this->targetName = $targetName;
|
||||
$this->selectedServerUuid = $selectedServerUuid;
|
||||
}
|
||||
|
||||
public function handleUploadCompleted()
|
||||
{
|
||||
// Refresh the component after upload
|
||||
$this->dispatch('success', 'File uploaded successfully!');
|
||||
}
|
||||
|
||||
public function generateFilePath()
|
||||
{
|
||||
if (empty($this->filename)) {
|
||||
$this->dispatch('error', 'No file uploaded yet.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Determine server UUID
|
||||
$serverUuid = $this->selectedServerUuid ?? $this->selectedUuid;
|
||||
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->first();
|
||||
|
||||
if (! $server) {
|
||||
$this->dispatch('error', 'Server not found.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$isContainer = ($this->selectedServerUuid && $this->selectedUuid !== $this->selectedServerUuid);
|
||||
|
||||
// Generate unique file identifier
|
||||
$uploadId = uniqid('terminal_', true);
|
||||
$sanitizedFilename = basename($this->filename);
|
||||
$storageDir = "terminal-uploads/{$uploadId}";
|
||||
$storagePath = storage_path("app/{$storageDir}");
|
||||
|
||||
// Get the uploaded file from Livewire
|
||||
if (! $this->uploadedFile) {
|
||||
$this->dispatch('error', 'Uploaded file not found. Please try uploading again.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create directory and store file
|
||||
if (! file_exists($storagePath)) {
|
||||
mkdir($storagePath, 0755, true);
|
||||
}
|
||||
|
||||
$finalPath = "{$storagePath}/{$sanitizedFilename}";
|
||||
$this->uploadedFile->storeAs($storageDir, $sanitizedFilename);
|
||||
|
||||
// Copy file to server's temporary directory
|
||||
$serverTmpPath = "/tmp/coolify_import_{$uploadId}_{$sanitizedFilename}";
|
||||
instant_scp($finalPath, $serverTmpPath, $server);
|
||||
|
||||
// If it's a container, copy to container
|
||||
if ($isContainer) {
|
||||
$containerPath = "/tmp/{$sanitizedFilename}";
|
||||
|
||||
instant_remote_process([
|
||||
"docker cp {$serverTmpPath} {$this->selectedUuid}:{$containerPath}",
|
||||
], $server);
|
||||
|
||||
$this->filePath = $containerPath;
|
||||
} else {
|
||||
$this->filePath = $serverTmpPath;
|
||||
}
|
||||
|
||||
// Schedule cleanup job
|
||||
CleanupExpiredTerminalFilesJob::dispatch(
|
||||
$finalPath,
|
||||
$serverTmpPath,
|
||||
$server->id,
|
||||
$isContainer ? $this->selectedUuid : null,
|
||||
$sanitizedFilename
|
||||
)->delay(now()->addMinutes($this->expirationMinutes));
|
||||
|
||||
$this->dispatch('success', "File ready! Path: {$this->filePath}");
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.terminal.file-import');
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for Livewire toJSON serialization issue
|
||||
* This prevents "Public method [toJSON] not found" errors
|
||||
*/
|
||||
public function toJSON()
|
||||
{
|
||||
return json_encode([
|
||||
'selectedUuid' => $this->selectedUuid,
|
||||
'targetName' => $this->targetName,
|
||||
'selectedServerUuid' => $this->selectedServerUuid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,8 @@ class Index extends Component
|
|||
|
||||
public bool $isLoadingContainers = true;
|
||||
|
||||
public bool $showImportModal = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->servers = Server::isReachable()->get()->filter(function ($server) {
|
||||
|
|
@ -84,6 +86,58 @@ class Index extends Component
|
|||
);
|
||||
}
|
||||
|
||||
public function getTargetName()
|
||||
{
|
||||
if ($this->selected_uuid === 'default') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Check if it's a server
|
||||
$server = collect($this->servers)->firstWhere('uuid', $this->selected_uuid);
|
||||
if ($server) {
|
||||
return $server->name.' (Server)';
|
||||
}
|
||||
|
||||
// Otherwise it's a container
|
||||
$container = collect($this->containers)->firstWhere('uuid', $this->selected_uuid);
|
||||
if ($container) {
|
||||
return $container['name'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getServerUuid()
|
||||
{
|
||||
if ($this->selected_uuid === 'default') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if it's a server
|
||||
$server = collect($this->servers)->firstWhere('uuid', $this->selected_uuid);
|
||||
if ($server) {
|
||||
return $server->uuid;
|
||||
}
|
||||
|
||||
// Otherwise it's a container, get its server
|
||||
$container = collect($this->containers)->firstWhere('uuid', $this->selected_uuid);
|
||||
if ($container) {
|
||||
return $container['server_uuid'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function openImportModal()
|
||||
{
|
||||
$this->showImportModal = true;
|
||||
}
|
||||
|
||||
public function closeImportModal()
|
||||
{
|
||||
$this->showImportModal = false;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.terminal.index');
|
||||
|
|
|
|||
124
resources/views/livewire/terminal/file-import.blade.php
Normal file
124
resources/views/livewire/terminal/file-import.blade.php
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<div x-data="{ error: $wire.entangle('error'), filesize: $wire.entangle('filesize'), filename: $wire.entangle('filename'), isUploading: $wire.entangle('isUploading'), progress: $wire.entangle('progress'), filePath: $wire.entangle('filePath') }">
|
||||
|
||||
<div class="pb-4">
|
||||
<h2>Import File for Terminal</h2>
|
||||
<div class="text-sm text-neutral-500 pb-2">
|
||||
Upload a file that will be temporarily stored and accessible in your selected server or container.
|
||||
Perfect for importing SQL dumps, configuration files, or any other data.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Target Display -->
|
||||
@if ($targetName)
|
||||
<div class="rounded-sm bg-coolgray-100 dark:bg-coolgray-200 p-3">
|
||||
<div class="text-sm">
|
||||
<span class="font-semibold">Target:</span> {{ $targetName }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Expiration Time Selection -->
|
||||
<div class="w-full lg:w-64">
|
||||
<x-forms.select
|
||||
id="expirationMinutes"
|
||||
label="File Expiration"
|
||||
wire:model="expirationMinutes">
|
||||
@foreach ($this->expirationOptions as $minutes => $label)
|
||||
<option value="{{ $minutes }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
<div class="text-xs text-neutral-500 mt-1">
|
||||
File will be automatically deleted after this time for security.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Upload -->
|
||||
<div>
|
||||
<h3 class="pb-2">Upload File</h3>
|
||||
<div class="border-2 border-dashed border-coolgray-300 dark:border-coolgray-400 rounded-lg p-6 text-center hover:border-coolgray-400 dark:hover:border-coolgray-300 transition-colors">
|
||||
<input
|
||||
type="file"
|
||||
wire:model="uploadedFile"
|
||||
id="file-upload"
|
||||
class="hidden"
|
||||
@change="
|
||||
const file = $event.target.files[0];
|
||||
if (file) {
|
||||
filename = file.name;
|
||||
filesize = Number(file.size / 1024 / 1024).toFixed(2) + ' MB';
|
||||
}
|
||||
"
|
||||
>
|
||||
<label for="file-upload" class="cursor-pointer">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-coolgray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
<div class="text-sm text-coolgray-500">
|
||||
<span class="font-semibold text-blue-500 hover:text-blue-600">Click to upload</span>
|
||||
or drag and drop
|
||||
</div>
|
||||
<div class="text-xs text-coolgray-400">
|
||||
Any file up to 10GB
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Progress -->
|
||||
<div x-show="isUploading" x-cloak>
|
||||
<div class="text-sm pb-1">Uploading: <span x-text="Math.round(progress)"></span>%</div>
|
||||
<progress max="100" x-bind:value="progress" class="progress progress-warning w-full"></progress>
|
||||
</div>
|
||||
|
||||
<!-- File Information -->
|
||||
<div x-show="filename && !error && !filePath" x-cloak class="rounded-sm bg-coolgray-100 dark:bg-coolgray-200 p-4">
|
||||
<h3 class="pb-2">File Uploaded</h3>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div><span class="font-semibold">Filename:</span> <span x-text="filename"></span></div>
|
||||
<div><span class="font-semibold">Size:</span> <span x-text="filesize"></span></div>
|
||||
</div>
|
||||
<x-forms.button
|
||||
class="mt-4 w-full"
|
||||
wire:click='generateFilePath'>
|
||||
Generate File Path & Copy to Target
|
||||
</x-forms.button>
|
||||
</div>
|
||||
|
||||
<!-- File Path Result -->
|
||||
<div x-show="filePath" x-cloak class="rounded-sm bg-success/10 border border-success p-4">
|
||||
<h3 class="pb-2 text-success">File Ready!</h3>
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm">
|
||||
<span class="font-semibold">File Path:</span>
|
||||
<code class="block mt-1 p-2 bg-coolgray-100 dark:bg-coolgray-300 rounded text-xs" x-text="filePath"></code>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="font-semibold">Expires in:</span> {{ $expirationMinutes }} minutes
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600 dark:text-neutral-400 pt-2">
|
||||
Copy the file path above and use it in your terminal commands.
|
||||
The file will be automatically deleted after expiration.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security Notice -->
|
||||
<div class="rounded-sm alert-warning text-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 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>
|
||||
<div>
|
||||
<div class="font-semibold">Security Notice</div>
|
||||
<div class="text-xs mt-1">
|
||||
Uploaded files are stored temporarily and will be automatically deleted after the expiration time.
|
||||
Do not upload sensitive files without encryption. Always verify file permissions after upload.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -15,27 +15,65 @@
|
|||
</div>
|
||||
@else
|
||||
@if ($servers->count() > 0)
|
||||
<form class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row"
|
||||
wire:submit="$dispatchSelf('connectToContainer')">
|
||||
<x-forms.select id="selected_uuid" required wire:model.live="selected_uuid">
|
||||
<option value="default">Select a server or container</option>
|
||||
@foreach ($servers as $server)
|
||||
<option value="{{ $server->uuid }}">{{ $server->name }}</option>
|
||||
@foreach ($containers as $container)
|
||||
@if ($container['server_uuid'] == $server->uuid)
|
||||
<option value="{{ $container['uuid'] }}">
|
||||
{{ $server->name }} -> {{ $container['name'] }}
|
||||
</option>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row">
|
||||
<form class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row flex-1"
|
||||
wire:submit="$dispatchSelf('connectToContainer')">
|
||||
<x-forms.select id="selected_uuid" required wire:model.live="selected_uuid">
|
||||
<option value="default">Select a server or container</option>
|
||||
@foreach ($servers as $server)
|
||||
<option value="{{ $server->uuid }}">{{ $server->name }}</option>
|
||||
@foreach ($containers as $container)
|
||||
@if ($container['server_uuid'] == $server->uuid)
|
||||
<option value="{{ $container['uuid'] }}">
|
||||
{{ $server->name }} -> {{ $container['name'] }}
|
||||
</option>
|
||||
@endif
|
||||
@endforeach
|
||||
@endforeach
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
<x-forms.button type="submit">Connect</x-forms.button>
|
||||
</form>
|
||||
</x-forms.select>
|
||||
<x-forms.button type="submit">Connect</x-forms.button>
|
||||
</form>
|
||||
@if ($selected_uuid !== 'default')
|
||||
<x-forms.button wire:click="openImportModal">Import File</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div>No servers with terminal access found.</div>
|
||||
@endif
|
||||
@endif
|
||||
<livewire:project.shared.terminal />
|
||||
</div>
|
||||
|
||||
@if ($showImportModal && $selected_uuid !== 'default')
|
||||
<div class="fixed top-0 left-0 z-99 flex items-center justify-center w-screen h-screen p-4"
|
||||
@keydown.window.escape="$wire.closeImportModal()">
|
||||
<div class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"
|
||||
wire:click="closeImportModal">
|
||||
</div>
|
||||
<div class="relative w-full border rounded-sm drop-shadow-sm min-w-full lg:min-w-[36rem] max-w-fit max-h-[calc(100vh-2rem)] bg-white border-neutral-200 dark:bg-base dark:border-coolgray-300 flex flex-col">
|
||||
<div class="flex items-center justify-between py-6 px-6 shrink-0">
|
||||
<h3 class="text-2xl font-bold">Import File to Terminal</h3>
|
||||
<button wire:click="closeImportModal"
|
||||
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 rounded-full dark:text-white hover:bg-neutral-100 dark:hover:bg-coolgray-300">
|
||||
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative flex items-center justify-center w-auto overflow-y-auto px-6 pb-6">
|
||||
@php
|
||||
$targetName = $this->getTargetName();
|
||||
$serverUuid = $this->getServerUuid();
|
||||
@endphp
|
||||
<livewire:terminal.file-import
|
||||
:selectedUuid="$selected_uuid"
|
||||
:targetName="$targetName"
|
||||
:selectedServerUuid="$serverUuid"
|
||||
:key="'file-import-'.$selected_uuid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
@ -295,6 +295,7 @@ Route::middleware(['auth'])->group(function () {
|
|||
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::post('/upload/backup/{databaseUuid}', [UploadController::class, 'upload'])->name('upload.backup');
|
||||
Route::post('/upload/terminal', [UploadController::class, 'uploadTerminalFile'])->name('upload.terminal');
|
||||
Route::get('/download/backup/{executionId}', function () {
|
||||
try {
|
||||
$user = auth()->user();
|
||||
|
|
|
|||
Loading…
Reference in a new issue