215 lines
6.2 KiB
PHP
215 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Files;
|
|
|
|
use App\Livewire\Concerns\WithFleetContext;
|
|
use App\Services\FleetService;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Attributes\On;
|
|
use Livewire\Attributes\Title;
|
|
use Livewire\Component;
|
|
use Livewire\WithFileUploads;
|
|
use Throwable;
|
|
|
|
#[Layout('layouts.app')]
|
|
#[Title('Dateien — Clusev')]
|
|
class Index extends Component
|
|
{
|
|
use WithFileUploads, WithFleetContext;
|
|
|
|
/** Current working directory in the SFTP browser. */
|
|
public string $path = '/';
|
|
|
|
public bool $connected = false;
|
|
|
|
public bool $ready = false;
|
|
|
|
public $upload = null;
|
|
|
|
/** @var array<int, array{name:string,type:string,size:?int,perms:string,owner:string,modified:string}> */
|
|
public array $entries = [];
|
|
|
|
public function mount(): void
|
|
{
|
|
// listing loads lazily via wire:init -> load()
|
|
}
|
|
|
|
/** Read the active server's directory over SSH; empty + offline on failure. */
|
|
public function load(): void
|
|
{
|
|
$this->entries = [];
|
|
$this->connected = false;
|
|
|
|
$active = $this->activeServer();
|
|
if ($active && $active->credential_exists) {
|
|
try {
|
|
$this->entries = app(FleetService::class)->files($active, $this->path);
|
|
$this->connected = true;
|
|
} catch (Throwable) {
|
|
$this->connected = false;
|
|
}
|
|
}
|
|
|
|
$this->ready = true;
|
|
}
|
|
|
|
public function open(int $index): void
|
|
{
|
|
$name = $this->entries[$index]['name'] ?? null;
|
|
if ($name === null) {
|
|
return;
|
|
}
|
|
|
|
$this->path = rtrim($this->path, '/').'/'.$name;
|
|
$this->load();
|
|
}
|
|
|
|
public function go(int $index): void
|
|
{
|
|
$path = $this->crumbs[$index]['path'] ?? '/';
|
|
$this->path = $path !== '' ? $path : '/';
|
|
$this->load();
|
|
}
|
|
|
|
public function up(): void
|
|
{
|
|
$this->path = dirname($this->path) ?: '/';
|
|
$this->load();
|
|
}
|
|
|
|
/** Upload the selected file into the current directory (SFTP put). */
|
|
public function updatedUpload(FleetService $fleet): void
|
|
{
|
|
$this->validate(['upload' => ['file', 'max:51200']]); // 50 MB
|
|
|
|
$active = $this->activeServer();
|
|
if ($active && $active->credential_exists && $this->upload) {
|
|
try {
|
|
$name = basename($this->upload->getClientOriginalName());
|
|
$fleet->uploadFile($active, rtrim($this->path, '/').'/'.$name, $this->upload->getRealPath());
|
|
$this->dispatch('notify', message: "„{$name}“ hochgeladen.");
|
|
} catch (Throwable $e) {
|
|
$this->dispatch('notify', message: 'Upload fehlgeschlagen: '.$e->getMessage());
|
|
}
|
|
}
|
|
|
|
$this->reset('upload');
|
|
$this->load();
|
|
}
|
|
|
|
/** Stream a remote file to the browser as a download (SFTP get). */
|
|
public function download(int $index, FleetService $fleet)
|
|
{
|
|
$name = $this->entries[$index]['name'] ?? null;
|
|
$active = $this->activeServer();
|
|
if ($name === null || ! $active || ! $active->credential_exists) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$content = $fleet->getFile($active, rtrim($this->path, '/').'/'.basename($name));
|
|
} catch (Throwable $e) {
|
|
$this->dispatch('notify', message: $e->getMessage());
|
|
|
|
return null;
|
|
}
|
|
|
|
return response()->streamDownload(fn () => print($content), basename($name));
|
|
}
|
|
|
|
/** Open the view/edit modal for a file. */
|
|
public function edit(int $index): void
|
|
{
|
|
$name = $this->entries[$index]['name'] ?? null;
|
|
$active = $this->activeServer();
|
|
if ($name === null || ! $active) {
|
|
return;
|
|
}
|
|
|
|
$this->dispatch('openModal',
|
|
component: 'modals.file-editor',
|
|
arguments: [
|
|
'serverId' => $active->id,
|
|
'path' => rtrim($this->path, '/').'/'.basename($name),
|
|
'name' => basename($name),
|
|
],
|
|
);
|
|
}
|
|
|
|
#[On('fileSaved')]
|
|
public function reloadAfterSave(): void
|
|
{
|
|
$this->load();
|
|
}
|
|
|
|
/**
|
|
* Breadcrumb segments derived from $path (root first, then each folder).
|
|
*
|
|
* @return array<int, array{label: string, path: string}>
|
|
*/
|
|
public function getCrumbsProperty(): array
|
|
{
|
|
$crumbs = [['label' => '/', 'path' => '/']];
|
|
$acc = '';
|
|
|
|
foreach (array_filter(explode('/', $this->path)) as $segment) {
|
|
$acc .= '/'.$segment;
|
|
$crumbs[] = ['label' => $segment, 'path' => $acc];
|
|
}
|
|
|
|
return $crumbs;
|
|
}
|
|
|
|
/**
|
|
* Delete a file (R5): opens the confirm modal, which writes the AuditEvent
|
|
* and re-dispatches `fileConfirmed`. SFTP unlink is wired in behind this later.
|
|
*/
|
|
public function confirmDelete(int $index): void
|
|
{
|
|
$name = $this->entries[$index]['name'] ?? null;
|
|
if ($name === null) {
|
|
return;
|
|
}
|
|
|
|
$this->dispatch('openModal',
|
|
component: 'modals.confirm-action',
|
|
arguments: [
|
|
'heading' => 'Datei löschen',
|
|
'body' => "„{$name}“ wird unwiderruflich aus {$this->path} entfernt.",
|
|
'confirmLabel' => 'Löschen',
|
|
'danger' => true,
|
|
'icon' => 'trash',
|
|
'auditAction' => 'file.delete',
|
|
'auditTarget' => rtrim($this->path, '/')."/{$name}",
|
|
'event' => 'fileConfirmed',
|
|
'params' => ['name' => $name],
|
|
'notify' => "„{$name}“ gelöscht.",
|
|
],
|
|
);
|
|
}
|
|
|
|
/** Applies the confirmed deletion over SFTP, then reloads the directory. */
|
|
#[On('fileConfirmed')]
|
|
public function deleteEntry(string $name, FleetService $fleet): void
|
|
{
|
|
$active = $this->activeServer();
|
|
|
|
if ($active && $active->credential_exists) {
|
|
try {
|
|
$fleet->deleteFile($active, rtrim($this->path, '/').'/'.$name);
|
|
} catch (Throwable $e) {
|
|
$this->dispatch('notify', message: 'Löschen fehlgeschlagen: '.$e->getMessage());
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
$this->load();
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('livewire.files.index');
|
|
}
|
|
}
|