1633 lines
64 KiB
PHP
1633 lines
64 KiB
PHP
<?php
|
|
|
|
namespace ServerManager\UI;
|
|
|
|
use PHPNative\Ui\Widget\Button;
|
|
use PHPNative\Ui\Widget\Container;
|
|
use PHPNative\Ui\Widget\FileBrowser;
|
|
use PHPNative\Ui\Widget\Icon;
|
|
use PHPNative\Ui\Widget\Label;
|
|
use PHPNative\Ui\Widget\Modal;
|
|
use PHPNative\Ui\Widget\TextArea;
|
|
use PHPNative\Ui\Widget\TextInput;
|
|
use PHPNative\Ui\Widget\ProgressBar;
|
|
|
|
class SftpManagerTab
|
|
{
|
|
private Container $tab;
|
|
private FileBrowser $localFileBrowser;
|
|
private FileBrowser $remoteFileBrowser;
|
|
private Label $connectionStatusLabel;
|
|
private Modal $editModal;
|
|
private TextArea $fileEditor;
|
|
private Label $filePathLabel;
|
|
private string $currentEditFilePath = '';
|
|
private Modal $filenameModal;
|
|
private TextInput $filenameInput;
|
|
private Modal $renameModal;
|
|
private TextInput $renameInput;
|
|
private string $currentRenameFilePath = '';
|
|
private Modal $deleteConfirmModal;
|
|
private string $currentDeleteFilePath = '';
|
|
private Label $deleteConfirmLabel;
|
|
private null|array $currentLocalSelection = null;
|
|
private null|array $currentRemoteSelection = null;
|
|
private ?string $lastRemoteClickPath = null;
|
|
private float $lastRemoteClickTime = 0.0;
|
|
private ProgressBar $transferProgressBar;
|
|
private Label $transferInfoLabel;
|
|
private Label $transferBytesLabel;
|
|
private array $pendingUploadQueue = [];
|
|
private int $totalUploadFiles = 0;
|
|
private int $completedUploadFiles = 0;
|
|
private int $totalUploadBytes = 0;
|
|
private int $completedUploadBytes = 0;
|
|
private array $pendingDownloadQueue = [];
|
|
private int $totalDownloadFiles = 0;
|
|
private int $completedDownloadFiles = 0;
|
|
private int $totalDownloadBytes = 0;
|
|
private int $completedDownloadBytes = 0;
|
|
|
|
public function __construct(
|
|
string &$apiKey,
|
|
string &$privateKeyPath,
|
|
string &$remoteStartDir,
|
|
ServerListTab $serverListTab,
|
|
\PHPNative\Ui\Widget\TabContainer $tabContainer,
|
|
Label $statusLabel,
|
|
) {
|
|
$this->tab = new Container('flex flex-row p-4 gap-4 bg-gray-50');
|
|
$currentPrivateKeyPath = &$privateKeyPath;
|
|
$currentRemoteStartDir = &$remoteStartDir;
|
|
|
|
// Left side: Local file browser
|
|
// Lokaler Browser: Spalte, Scrollen übernimmt der FileBrowser selbst
|
|
$localBrowserContainer = new Container('flex flex-col flex-1 gap-2');
|
|
$localBrowserContainer->addComponent(new Label('Lokal', 'text-lg font-bold text-black mb-2'));
|
|
$this->localFileBrowser = new FileBrowser(
|
|
getcwd(),
|
|
false,
|
|
'flex-1 bg-white border-2 border-gray-300 rounded p-2',
|
|
);
|
|
$localBrowserContainer->addComponent($this->localFileBrowser);
|
|
|
|
// Track local file selection for uploads
|
|
$sftpTab = $this;
|
|
$this->localFileBrowser->setOnFileSelect(function ($path, $row) use ($sftpTab) {
|
|
$sftpTab->currentLocalSelection = [
|
|
'path' => $path,
|
|
'row' => $row,
|
|
];
|
|
});
|
|
|
|
// Right side: Remote file browser
|
|
// Remote-Browser: Spalte, Scrollen übernimmt der FileBrowser selbst
|
|
$remoteBrowserContainer = new Container('flex flex-col flex-1 gap-2');
|
|
|
|
// Header with title and new file button
|
|
$remoteHeader = new Container('flex flex-row items-center gap-2 mb-2');
|
|
$remoteHeader->addComponent(new Label('Remote', 'text-lg font-bold text-black flex-1'));
|
|
|
|
$newFileButton = new Button('', 'px-3 py-2 bg-green-600 text-white rounded hover:bg-green-700 flex items-center justify-center');
|
|
$newFileIcon = new Icon(\PHPNative\Tailwind\Data\Icon::plus, 16, 'text-white');
|
|
$newFileButton->setIcon($newFileIcon);
|
|
$remoteHeader->addComponent($newFileButton);
|
|
$remoteBrowserContainer->addComponent($remoteHeader);
|
|
|
|
$this->remoteFileBrowser = new FileBrowser('/', true, 'flex-1 bg-white border-2 border-gray-300 rounded p-2');
|
|
$remoteBrowserContainer->addComponent($this->remoteFileBrowser);
|
|
|
|
// Connection status label
|
|
$this->connectionStatusLabel = new Label('Nicht verbunden', 'text-sm text-gray-600 italic mb-2');
|
|
$remoteBrowserContainer->addComponent($this->connectionStatusLabel);
|
|
|
|
// Middle: Transfer buttons (Upload/Download)
|
|
$transferContainer = new Container('flex flex-col justify-center items-center gap-2 w-[180]');
|
|
|
|
$uploadButton = new Button(
|
|
'Hochladen →',
|
|
'w-full px-3 py-2 bg-green-600 text-white rounded hover:bg-green-700',
|
|
);
|
|
$downloadButton = new Button(
|
|
'← Herunterladen',
|
|
'w-full px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700',
|
|
);
|
|
$transferContainer->addComponent($uploadButton);
|
|
$transferContainer->addComponent($downloadButton);
|
|
|
|
// Transfer-Info + ProgressBar
|
|
$this->transferInfoLabel = new Label('Kein Transfer aktiv', 'text-xs text-gray-600 mt-2');
|
|
$this->transferBytesLabel = new Label('', 'text-xs text-gray-600');
|
|
$this->transferProgressBar = new ProgressBar('mt-1');
|
|
$this->transferProgressBar->setValue(0.0);
|
|
$transferContainer->addComponent($this->transferInfoLabel);
|
|
$transferContainer->addComponent($this->transferBytesLabel);
|
|
$transferContainer->addComponent($this->transferProgressBar);
|
|
|
|
$this->tab->addComponent($localBrowserContainer);
|
|
$this->tab->addComponent($transferContainer);
|
|
$this->tab->addComponent($remoteBrowserContainer);
|
|
|
|
// Setup remote navigation handler
|
|
$this->setupRemoteNavigationHandler($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
|
|
|
// Setup SFTP connection handler
|
|
$this->setupSftpConnectionHandler($currentPrivateKeyPath, $currentRemoteStartDir, $serverListTab, $tabContainer, $statusLabel);
|
|
|
|
// Create edit modal
|
|
$this->createEditModal($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
|
|
|
// Create filename input modal
|
|
$this->createFilenameModal($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
|
|
|
// Create rename modal
|
|
$this->createRenameModal($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
|
|
|
// Create delete confirmation modal
|
|
$this->createDeleteConfirmModal($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
|
|
|
// Setup transfer button handlers
|
|
$uploadButton->setOnClick(function () use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel) {
|
|
$sftpTab->handleUpload($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
|
});
|
|
|
|
$downloadButton->setOnClick(function () use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel) {
|
|
$sftpTab->handleDownload($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
|
});
|
|
|
|
// Setup file edit handler
|
|
$this->remoteFileBrowser->setOnEditFile(function ($path, $row) use (
|
|
&$currentPrivateKeyPath,
|
|
$serverListTab,
|
|
$statusLabel,
|
|
) {
|
|
$this->handleFileEdit($path, $row, $currentPrivateKeyPath, $serverListTab, $statusLabel);
|
|
});
|
|
|
|
// Setup new file button handler
|
|
$sftpTab = $this;
|
|
$newFileButton->setOnClick(function () use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel) {
|
|
$sftpTab->handleNewFile($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
|
});
|
|
|
|
// Setup rename handler
|
|
$this->remoteFileBrowser->setOnRenameFile(function ($path, $row) use (
|
|
$sftpTab,
|
|
&$currentPrivateKeyPath,
|
|
$serverListTab,
|
|
$statusLabel,
|
|
) {
|
|
$sftpTab->handleFileRename($path, $row, $currentPrivateKeyPath, $serverListTab, $statusLabel);
|
|
});
|
|
|
|
// Setup delete handler
|
|
$this->remoteFileBrowser->setOnDeleteFile(function ($path, $row) use (
|
|
$sftpTab,
|
|
&$currentPrivateKeyPath,
|
|
$serverListTab,
|
|
$statusLabel,
|
|
) {
|
|
$sftpTab->handleFileDelete($path, $row, $currentPrivateKeyPath, $serverListTab, $statusLabel);
|
|
});
|
|
}
|
|
|
|
private function setupRemoteNavigationHandler(
|
|
string &$currentPrivateKeyPath,
|
|
ServerListTab $serverListTab,
|
|
Label $statusLabel,
|
|
): void {
|
|
$sftpTab = $this;
|
|
// Create reference to selectedServer array outside of closure
|
|
$selectedServerRef = &$serverListTab->selectedServer;
|
|
|
|
$this->remoteFileBrowser->setOnFileSelect(
|
|
function ($path, $row) use (
|
|
$sftpTab,
|
|
&$currentPrivateKeyPath,
|
|
&$selectedServerRef,
|
|
$statusLabel,
|
|
) {
|
|
// Track remote selection (for downloads)
|
|
$sftpTab->currentRemoteSelection = [
|
|
'path' => $path,
|
|
'row' => $row,
|
|
];
|
|
|
|
// Only navigate when a directory is selected
|
|
if (!isset($row['isDir']) || !$row['isDir']) {
|
|
return;
|
|
}
|
|
|
|
// Require double click for navigation:
|
|
// first click selektiert nur, zweiter Klick (innerhalb kurzer Zeit)
|
|
// öffnet das Verzeichnis.
|
|
$now = microtime(true);
|
|
$doubleClickThreshold = 0.4; // Sekunden
|
|
|
|
if (
|
|
$sftpTab->lastRemoteClickPath !== $path ||
|
|
($now - $sftpTab->lastRemoteClickTime) > $doubleClickThreshold
|
|
) {
|
|
$sftpTab->lastRemoteClickPath = $path;
|
|
$sftpTab->lastRemoteClickTime = $now;
|
|
return;
|
|
}
|
|
|
|
// Zweiter Klick innerhalb des Zeitfensters -> als Doppelklick werten
|
|
$sftpTab->lastRemoteClickPath = null;
|
|
$sftpTab->lastRemoteClickTime = 0.0;
|
|
|
|
$loadButton = new Button('Load', '');
|
|
$loadButton->setOnClickAsync(
|
|
function () use ($path, &$currentPrivateKeyPath, &$selectedServerRef) {
|
|
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
|
|
return ['error' => 'Not connected'];
|
|
}
|
|
|
|
// Copy to local variable for async context
|
|
$selectedServer = $selectedServerRef;
|
|
|
|
try {
|
|
$sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']);
|
|
$key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath));
|
|
|
|
if (!$sftp->login('root', $key)) {
|
|
return ['error' => 'SFTP Login failed'];
|
|
}
|
|
|
|
$files = $sftp->nlist($path);
|
|
if ($files === false) {
|
|
return ['error' => 'Cannot read directory'];
|
|
}
|
|
|
|
$fileList = [];
|
|
foreach ($files as $file) {
|
|
if ($file === '.' || $file === '..') {
|
|
continue;
|
|
}
|
|
$fullPath = rtrim($path, '/') . '/' . $file;
|
|
$stat = $sftp->stat($fullPath);
|
|
$fileList[] = [
|
|
'name' => $file,
|
|
'path' => $fullPath,
|
|
'isDir' => ($stat['type'] ?? 0) === 2,
|
|
'size' => $stat['size'] ?? 0,
|
|
'mtime' => $stat['mtime'] ?? 0,
|
|
];
|
|
}
|
|
|
|
return ['success' => true, 'path' => $path, 'files' => $fileList];
|
|
} catch (\Exception $e) {
|
|
return ['error' => $e->getMessage()];
|
|
}
|
|
},
|
|
function ($result) use ($sftpTab, $statusLabel) {
|
|
if (isset($result['error'])) {
|
|
$statusLabel->setText('SFTP Fehler: ' . $result['error']);
|
|
return;
|
|
}
|
|
|
|
if (isset($result['success'])) {
|
|
$sftpTab->remoteFileBrowser->setPath($result['path']);
|
|
$sftpTab->remoteFileBrowser->setFileData($result['files']);
|
|
}
|
|
},
|
|
function ($error) use ($statusLabel) {
|
|
$errorMsg = is_string($error) ? $error : 'Unknown error';
|
|
$statusLabel->setText('SFTP Error: ' . $errorMsg);
|
|
},
|
|
);
|
|
|
|
$loadButton->handleMouseClick(0, 0, 0);
|
|
},
|
|
);
|
|
}
|
|
|
|
private function setupSftpConnectionHandler(
|
|
string &$currentPrivateKeyPath,
|
|
string &$currentRemoteStartDir,
|
|
ServerListTab $serverListTab,
|
|
\PHPNative\Ui\Widget\TabContainer $tabContainer,
|
|
Label $statusLabel,
|
|
): void {
|
|
$sftpTab = $this;
|
|
|
|
// Create a reference to selectedServer that we can update
|
|
$selectedServerRef = &$serverListTab->selectedServer;
|
|
|
|
// Set up async handler for SFTP connection (tab switch happens in success callback)
|
|
$serverListTab->getSftpButton()->setOnClickAsync(
|
|
function () use (&$currentPrivateKeyPath, &$currentRemoteStartDir, &$selectedServerRef) {
|
|
if ($selectedServerRef === null) {
|
|
return ['error' => 'Kein Server ausgewählt'];
|
|
}
|
|
|
|
// Copy the selected server data to a local variable for use in async context
|
|
$selectedServer = $selectedServerRef;
|
|
|
|
if (empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) {
|
|
return ['error' => 'Private Key Pfad nicht konfiguriert oder Datei nicht gefunden'];
|
|
}
|
|
|
|
try {
|
|
$ssh = new \phpseclib3\Net\SSH2($selectedServer['ipv4']);
|
|
$key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath));
|
|
|
|
if (!$ssh->login('root', $key)) {
|
|
return ['error' => 'SSH Login fehlgeschlagen'];
|
|
}
|
|
|
|
$sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']);
|
|
if (!$sftp->login('root', $key)) {
|
|
return ['error' => 'SFTP Login fehlgeschlagen'];
|
|
}
|
|
|
|
// Use configured start directory or fallback to root
|
|
$startDir = !empty($currentRemoteStartDir) ? $currentRemoteStartDir : '/';
|
|
|
|
// Check if start directory exists, fallback to root if not
|
|
if (!$sftp->is_dir($startDir)) {
|
|
$startDir = '/';
|
|
}
|
|
|
|
$files = $sftp->nlist($startDir);
|
|
if ($files === false) {
|
|
return ['error' => 'Kann Verzeichnis nicht lesen: ' . $startDir];
|
|
}
|
|
|
|
$fileList = [];
|
|
foreach ($files as $file) {
|
|
if ($file === '.' || $file === '..') {
|
|
continue;
|
|
}
|
|
$fullPath = rtrim($startDir, '/') . '/' . $file;
|
|
$stat = $sftp->stat($fullPath);
|
|
$fileList[] = [
|
|
'name' => $file,
|
|
'path' => $fullPath,
|
|
'isDir' => ($stat['type'] ?? 0) === 2,
|
|
'size' => $stat['size'] ?? 0,
|
|
'mtime' => $stat['mtime'] ?? 0,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'server' => $selectedServer,
|
|
'files' => $fileList,
|
|
'path' => $startDir,
|
|
];
|
|
} catch (\Exception $e) {
|
|
return ['error' => 'Verbindung fehlgeschlagen: ' . $e->getMessage()];
|
|
}
|
|
},
|
|
function ($result) use ($sftpTab, $tabContainer, $statusLabel) {
|
|
if (isset($result['error'])) {
|
|
$statusLabel->setText('SFTP Fehler: ' . $result['error']);
|
|
return;
|
|
}
|
|
|
|
if (isset($result['success']) && $result['success']) {
|
|
// Switch to SFTP tab on successful connection
|
|
// Tab-Reihenfolge in App.php:
|
|
// 0 = Server, 1 = Kanban, 2 = SFTP Manager
|
|
$tabContainer->setActiveTab(2);
|
|
|
|
$sftpTab->connectionStatusLabel->setText(
|
|
'Verbunden mit: ' . $result['server']['name'] . ' (' . $result['server']['ipv4'] . ')',
|
|
);
|
|
$statusLabel->setText('SFTP Verbindung erfolgreich zu ' . $result['server']['name']);
|
|
|
|
$sftpTab->remoteFileBrowser->setPath($result['path'] ?? '/');
|
|
$sftpTab->remoteFileBrowser->setFileData($result['files']);
|
|
}
|
|
},
|
|
function ($error) use ($statusLabel) {
|
|
$errorMsg = is_string($error)
|
|
? $error
|
|
: (
|
|
is_object($error) && method_exists($error, 'getMessage')
|
|
? $error->getMessage()
|
|
: 'Unbekannter Fehler'
|
|
);
|
|
$statusLabel->setText('SFTP Async Fehler: ' . $errorMsg);
|
|
},
|
|
);
|
|
}
|
|
|
|
public function getContainer(): Container
|
|
{
|
|
return $this->tab;
|
|
}
|
|
|
|
public function getEditModal(): Modal
|
|
{
|
|
return $this->editModal;
|
|
}
|
|
|
|
public function getFilenameModal(): Modal
|
|
{
|
|
return $this->filenameModal;
|
|
}
|
|
|
|
public function getRenameModal(): Modal
|
|
{
|
|
return $this->renameModal;
|
|
}
|
|
|
|
public function getDeleteConfirmModal(): Modal
|
|
{
|
|
return $this->deleteConfirmModal;
|
|
}
|
|
|
|
private function createEditModal(
|
|
string &$currentPrivateKeyPath,
|
|
ServerListTab $serverListTab,
|
|
Label $statusLabel,
|
|
): void {
|
|
// Create modal content container
|
|
$modalContent = new Container('flex flex-col bg-white rounded-lg shadow-lg p-4 gap-3 w-[600] h-[400]');
|
|
// Disable texture caching for modal content to allow dynamic content updates
|
|
$modalContent->setUseTextureCache(false);
|
|
|
|
// File path label
|
|
$this->filePathLabel = new Label('', 'text-sm text-gray-600 font-mono');
|
|
$modalContent->addComponent($this->filePathLabel);
|
|
|
|
// Text editor
|
|
$this->fileEditor = new TextArea(
|
|
'',
|
|
'Lade Datei...',
|
|
'flex-1 border-2 border-gray-300 rounded p-2 bg-white text-black font-mono text-sm',
|
|
);
|
|
// Disable texture caching for TextArea to ensure text updates are visible
|
|
$this->fileEditor->setUseTextureCache(false);
|
|
$modalContent->addComponent($this->fileEditor);
|
|
|
|
// Button container
|
|
$buttonContainer = new Container('flex flex-row justify-end gap-2');
|
|
|
|
// Cancel button
|
|
$cancelButton = new Button(
|
|
'Abbrechen',
|
|
'px-4 py-2 bg-gray-300 text-black rounded hover:bg-gray-400',
|
|
);
|
|
$sftpTab = $this;
|
|
$cancelButton->setOnClick(function () use ($sftpTab) {
|
|
$sftpTab->editModal->setVisible(false);
|
|
});
|
|
$buttonContainer->addComponent($cancelButton);
|
|
|
|
// Save button
|
|
$saveButton = new Button('Speichern', 'px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600');
|
|
$saveButton->setOnClick(function () use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel) {
|
|
$sftpTab->handleFileSave($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
|
});
|
|
$buttonContainer->addComponent($saveButton);
|
|
|
|
$modalContent->addComponent($buttonContainer);
|
|
|
|
// Create modal (will be added to main container by App.php)
|
|
$this->editModal = new Modal($modalContent);
|
|
}
|
|
|
|
private function handleFileEdit(
|
|
string $path,
|
|
array $row,
|
|
string &$currentPrivateKeyPath,
|
|
ServerListTab $serverListTab,
|
|
Label $statusLabel,
|
|
): void {
|
|
$this->currentEditFilePath = $path;
|
|
$this->filePathLabel->setText('Bearbeite: ' . $path);
|
|
$this->fileEditor->setValue('');
|
|
$this->fileEditor->setFocused(false);
|
|
$this->editModal->setVisible(true);
|
|
|
|
// Reference to components for async callback
|
|
$selectedServerRef = &$serverListTab->selectedServer;
|
|
$sftpTab = $this;
|
|
|
|
// Load file content asynchronously
|
|
$loadButton = new Button('Load', '');
|
|
$loadButton->setOnClickAsync(
|
|
function () use ($path, &$currentPrivateKeyPath, &$selectedServerRef) {
|
|
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
|
|
return ['error' => 'Not connected'];
|
|
}
|
|
|
|
$selectedServer = $selectedServerRef;
|
|
|
|
try {
|
|
$sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']);
|
|
$key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath));
|
|
|
|
if (!$sftp->login('root', $key)) {
|
|
return ['error' => 'SFTP Login failed'];
|
|
}
|
|
|
|
$fileContent = $sftp->get($path);
|
|
if ($fileContent === false) {
|
|
return ['error' => 'Could not read file'];
|
|
}
|
|
|
|
return ['success' => true, 'content' => $fileContent];
|
|
} catch (\Exception $e) {
|
|
return ['error' => $e->getMessage()];
|
|
}
|
|
},
|
|
function ($result) use ($sftpTab, $statusLabel) {
|
|
if (isset($result['error'])) {
|
|
$statusLabel->setText('Fehler beim Laden: ' . $result['error']);
|
|
$sftpTab->editModal->setVisible(false);
|
|
return;
|
|
}
|
|
|
|
if (isset($result['success'])) {
|
|
$sftpTab->fileEditor->setValue($result['content']);
|
|
$sftpTab->fileEditor->setFocused(true);
|
|
$statusLabel->setText('Datei geladen');
|
|
}
|
|
},
|
|
function ($error) use ($statusLabel, $sftpTab) {
|
|
$errorMsg = is_string($error) ? $error : 'Unknown error';
|
|
$statusLabel->setText('Fehler: ' . $errorMsg);
|
|
$sftpTab->editModal->setVisible(false);
|
|
},
|
|
);
|
|
|
|
$loadButton->handleMouseClick(0, 0, 0);
|
|
}
|
|
|
|
private function handleFileSave(
|
|
string &$currentPrivateKeyPath,
|
|
ServerListTab $serverListTab,
|
|
Label $statusLabel,
|
|
): void {
|
|
$content = $this->fileEditor->getValue();
|
|
$path = $this->currentEditFilePath;
|
|
|
|
if (empty($path)) {
|
|
$statusLabel->setText('Fehler: Kein Dateipfad');
|
|
return;
|
|
}
|
|
|
|
// Reference to components for async callback
|
|
$selectedServerRef = &$serverListTab->selectedServer;
|
|
$sftpTab = $this;
|
|
|
|
// Save file content asynchronously
|
|
$saveButton = new Button('Save', '');
|
|
$saveButton->setOnClickAsync(
|
|
function () use ($path, $content, &$currentPrivateKeyPath, &$selectedServerRef) {
|
|
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
|
|
return ['error' => 'Not connected'];
|
|
}
|
|
|
|
$selectedServer = $selectedServerRef;
|
|
|
|
try {
|
|
$sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']);
|
|
$key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath));
|
|
|
|
if (!$sftp->login('root', $key)) {
|
|
return ['error' => 'SFTP Login failed'];
|
|
}
|
|
|
|
$result = $sftp->put($path, $content);
|
|
if ($result === false) {
|
|
return ['error' => 'Could not write file'];
|
|
}
|
|
|
|
return ['success' => true];
|
|
} catch (\Exception $e) {
|
|
return ['error' => $e->getMessage()];
|
|
}
|
|
},
|
|
function ($result) use ($sftpTab, $statusLabel, &$currentPrivateKeyPath, &$selectedServerRef) {
|
|
if (isset($result['error'])) {
|
|
$statusLabel->setText('Fehler beim Speichern: ' . $result['error']);
|
|
return;
|
|
}
|
|
|
|
if (isset($result['success'])) {
|
|
$statusLabel->setText('Datei erfolgreich gespeichert');
|
|
$sftpTab->editModal->setVisible(false);
|
|
|
|
// Reload the current directory to show the new/updated file
|
|
$sftpTab->reloadCurrentDirectory($currentPrivateKeyPath, $selectedServerRef, $statusLabel);
|
|
}
|
|
},
|
|
function ($error) use ($statusLabel) {
|
|
$errorMsg = is_string($error) ? $error : 'Unknown error';
|
|
$statusLabel->setText('Fehler: ' . $errorMsg);
|
|
},
|
|
);
|
|
|
|
$saveButton->handleMouseClick(0, 0, 0);
|
|
}
|
|
|
|
private function handleNewFile(
|
|
string &$currentPrivateKeyPath,
|
|
ServerListTab $serverListTab,
|
|
Label $statusLabel,
|
|
): void {
|
|
if ($serverListTab->selectedServer === null) {
|
|
$statusLabel->setText('Kein Server ausgewählt');
|
|
return;
|
|
}
|
|
|
|
// Show filename input modal
|
|
$this->filenameInput->setValue('neue_datei.txt');
|
|
$this->filenameModal->setVisible(true);
|
|
}
|
|
|
|
private function createFilenameModal(
|
|
string &$currentPrivateKeyPath,
|
|
ServerListTab $serverListTab,
|
|
Label $statusLabel,
|
|
): void {
|
|
// Create modal content
|
|
$modalContent = new Container('flex flex-col bg-white rounded-lg shadow-lg p-4 gap-3 w-96');
|
|
|
|
// Title
|
|
$modalContent->addComponent(new Label('Neue Datei', 'text-lg font-bold text-black'));
|
|
|
|
// Filename input
|
|
$this->filenameInput = new TextInput(
|
|
'Dateiname',
|
|
'w-full border-2 border-gray-300 rounded px-3 py-2 bg-white text-black',
|
|
);
|
|
$modalContent->addComponent($this->filenameInput);
|
|
|
|
// Button container
|
|
$buttonContainer = new Container('flex flex-row justify-end gap-2');
|
|
|
|
// Cancel button
|
|
$cancelButton = new Button('Abbrechen', 'px-4 py-2 bg-gray-300 text-black rounded hover:bg-gray-400');
|
|
$sftpTab = $this;
|
|
$cancelButton->setOnClick(function () use ($sftpTab) {
|
|
$sftpTab->filenameModal->setVisible(false);
|
|
});
|
|
$buttonContainer->addComponent($cancelButton);
|
|
|
|
// Create button
|
|
$createButton = new Button('Erstellen', 'px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700');
|
|
$createButton->setOnClick(function () use ($sftpTab, $statusLabel) {
|
|
$filename = trim($sftpTab->filenameInput->getValue());
|
|
|
|
if (empty($filename)) {
|
|
$statusLabel->setText('Dateiname darf nicht leer sein');
|
|
return;
|
|
}
|
|
|
|
// Close filename modal
|
|
$sftpTab->filenameModal->setVisible(false);
|
|
|
|
// Create the new file with the given name
|
|
$currentPath = $sftpTab->remoteFileBrowser->getCurrentPath();
|
|
$newFilePath = rtrim($currentPath, '/') . '/' . $filename;
|
|
|
|
$sftpTab->currentEditFilePath = $newFilePath;
|
|
$sftpTab->filePathLabel->setText('Neue Datei: ' . $newFilePath);
|
|
$sftpTab->fileEditor->setValue('');
|
|
$sftpTab->fileEditor->setFocused(true);
|
|
$sftpTab->editModal->setVisible(true);
|
|
|
|
$statusLabel->setText('Neue Datei wird erstellt: ' . $filename);
|
|
});
|
|
$buttonContainer->addComponent($createButton);
|
|
|
|
$modalContent->addComponent($buttonContainer);
|
|
|
|
// Create modal
|
|
$this->filenameModal = new Modal($modalContent);
|
|
}
|
|
|
|
private function reloadCurrentDirectory(
|
|
string &$currentPrivateKeyPath,
|
|
?array &$selectedServerRef,
|
|
Label $statusLabel,
|
|
): void {
|
|
$path = $this->remoteFileBrowser->getCurrentPath();
|
|
|
|
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
|
|
return;
|
|
}
|
|
|
|
$sftpTab = $this;
|
|
|
|
// Load directory asynchronously
|
|
$loadButton = new Button('Load', '');
|
|
$loadButton->setOnClickAsync(
|
|
function () use ($path, &$currentPrivateKeyPath, &$selectedServerRef) {
|
|
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
|
|
return ['error' => 'Not connected'];
|
|
}
|
|
|
|
$selectedServer = $selectedServerRef;
|
|
|
|
try {
|
|
$sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']);
|
|
$key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath));
|
|
|
|
if (!$sftp->login('root', $key)) {
|
|
return ['error' => 'SFTP Login failed'];
|
|
}
|
|
|
|
$files = $sftp->nlist($path);
|
|
if ($files === false) {
|
|
return ['error' => 'Cannot read directory'];
|
|
}
|
|
|
|
$fileList = [];
|
|
foreach ($files as $file) {
|
|
if ($file === '.' || $file === '..') {
|
|
continue;
|
|
}
|
|
$fullPath = rtrim($path, '/') . '/' . $file;
|
|
$stat = $sftp->stat($fullPath);
|
|
$fileList[] = [
|
|
'name' => $file,
|
|
'path' => $fullPath,
|
|
'isDir' => ($stat['type'] ?? 0) === 2,
|
|
'size' => $stat['size'] ?? 0,
|
|
'mtime' => $stat['mtime'] ?? 0,
|
|
];
|
|
}
|
|
|
|
return ['success' => true, 'path' => $path, 'files' => $fileList];
|
|
} catch (\Exception $e) {
|
|
return ['error' => $e->getMessage()];
|
|
}
|
|
},
|
|
function ($result) use ($sftpTab, $statusLabel) {
|
|
if (isset($result['error'])) {
|
|
$statusLabel->setText('Fehler beim Aktualisieren: ' . $result['error']);
|
|
return;
|
|
}
|
|
|
|
if (isset($result['success'])) {
|
|
$sftpTab->remoteFileBrowser->setPath($result['path']);
|
|
$sftpTab->remoteFileBrowser->setFileData($result['files']);
|
|
}
|
|
},
|
|
function ($error) use ($statusLabel) {
|
|
$errorMsg = is_string($error) ? $error : 'Unknown error';
|
|
$statusLabel->setText('Fehler beim Aktualisieren: ' . $errorMsg);
|
|
},
|
|
);
|
|
|
|
$loadButton->handleMouseClick(0, 0, 0);
|
|
}
|
|
|
|
private function handleUpload(
|
|
string &$currentPrivateKeyPath,
|
|
ServerListTab $serverListTab,
|
|
Label $statusLabel,
|
|
): void {
|
|
if ($this->currentLocalSelection === null) {
|
|
$statusLabel->setText('Keine lokale Datei ausgewählt');
|
|
return;
|
|
}
|
|
|
|
$localRow = $this->currentLocalSelection['row'] ?? null;
|
|
$localPath = $this->currentLocalSelection['path'] ?? null;
|
|
|
|
if ($localRow === null || $localPath === null) {
|
|
$statusLabel->setText('Ungültige lokale Auswahl');
|
|
return;
|
|
}
|
|
|
|
if ($serverListTab->selectedServer === null) {
|
|
$statusLabel->setText('Kein Server ausgewählt');
|
|
return;
|
|
}
|
|
|
|
if (empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) {
|
|
$statusLabel->setText('Private Key Pfad nicht konfiguriert oder Datei nicht gefunden');
|
|
return;
|
|
}
|
|
|
|
$remoteDir = $this->remoteFileBrowser->getCurrentPath();
|
|
if ($remoteDir === '') {
|
|
$remoteDir = '/';
|
|
}
|
|
|
|
// Upload-Queue inkl. Byte-Größen vorbereiten
|
|
$this->pendingUploadQueue = $this->buildUploadQueue($localPath, $localRow, $remoteDir);
|
|
$this->totalUploadFiles = count($this->pendingUploadQueue);
|
|
$this->completedUploadFiles = 0;
|
|
$this->totalUploadBytes = 0;
|
|
$this->completedUploadBytes = 0;
|
|
|
|
foreach ($this->pendingUploadQueue as $item) {
|
|
$this->totalUploadBytes += (int) ($item['size'] ?? 0);
|
|
}
|
|
|
|
$this->transferProgressBar->setValue(0.0);
|
|
$this->transferInfoLabel->setText(
|
|
sprintf(
|
|
'Upload: %d Dateien',
|
|
$this->totalUploadFiles,
|
|
),
|
|
);
|
|
$this->transferBytesLabel->setText(
|
|
sprintf(
|
|
'0.00 / %.2f MB',
|
|
$this->totalUploadBytes > 0 ? ($this->totalUploadBytes / (1024 * 1024)) : 0,
|
|
),
|
|
);
|
|
|
|
if ($this->totalUploadFiles <= 0) {
|
|
$this->transferProgressBar->setValue(1.0);
|
|
$this->transferInfoLabel->setText('Keine Dateien zum Hochladen');
|
|
return;
|
|
}
|
|
|
|
// Referenz auf ausgewählten Server für spätere Reloads
|
|
$this->startNextUploadTask($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
|
}
|
|
|
|
/**
|
|
* Lokale Upload-Statistik (Dateien/Ordner/Bytes) rekursiv berechnen.
|
|
*/
|
|
private function calculateLocalUploadStats(string $path, array $row): array
|
|
{
|
|
$files = 0;
|
|
$dirs = 0;
|
|
$bytes = 0;
|
|
|
|
$isDir = (bool) ($row['isDir'] ?? false);
|
|
|
|
if (!$isDir) {
|
|
if (is_file($path) && is_readable($path)) {
|
|
$files = 1;
|
|
$bytes = (int) @filesize($path);
|
|
}
|
|
return ['files' => $files, 'dirs' => $dirs, 'bytes' => $bytes];
|
|
}
|
|
|
|
// Ordner: rekursiv zählen
|
|
$dirs++;
|
|
|
|
$iterator = @scandir($path);
|
|
if ($iterator === false) {
|
|
return ['files' => $files, 'dirs' => $dirs, 'bytes' => $bytes];
|
|
}
|
|
|
|
foreach ($iterator as $entry) {
|
|
if ($entry === '.' || $entry === '..') {
|
|
continue;
|
|
}
|
|
|
|
$childPath = $path . DIRECTORY_SEPARATOR . $entry;
|
|
|
|
if (is_dir($childPath)) {
|
|
$childStats = $this->calculateLocalUploadStats($childPath, ['isDir' => true]);
|
|
$files += $childStats['files'];
|
|
$dirs += $childStats['dirs'];
|
|
$bytes += $childStats['bytes'];
|
|
} elseif (is_file($childPath) && is_readable($childPath)) {
|
|
$files++;
|
|
$bytes += (int) @filesize($childPath);
|
|
}
|
|
}
|
|
|
|
return ['files' => $files, 'dirs' => $dirs, 'bytes' => $bytes];
|
|
}
|
|
|
|
/**
|
|
* Queue für Datei-Uploads aufbauen (Datei- oder Ordner-Auswahl).
|
|
*
|
|
* @return array<int,array{local:string,remote:string}>
|
|
*/
|
|
private function buildUploadQueue(string $localPath, array $row, string $remoteDir): array
|
|
{
|
|
$queue = [];
|
|
$remoteBase = rtrim($remoteDir, '/');
|
|
if ($remoteBase === '') {
|
|
$remoteBase = '/';
|
|
}
|
|
|
|
$isDir = (bool) ($row['isDir'] ?? false);
|
|
if (!$isDir) {
|
|
$queue[] = [
|
|
'local' => $localPath,
|
|
'remote' => $remoteBase . '/' . basename($localPath),
|
|
'size' => is_file($localPath) ? (int) @filesize($localPath) : 0,
|
|
];
|
|
return $queue;
|
|
}
|
|
|
|
// Wurzelordner auf Remote
|
|
$rootRemote = $remoteBase . '/' . basename($localPath);
|
|
$this->collectUploadFilesRecursive($localPath, $rootRemote, $queue);
|
|
|
|
return $queue;
|
|
}
|
|
|
|
/**
|
|
* Rekursive Sammlung aller Dateien eines Ordners.
|
|
*
|
|
* @param array<int,array{local:string,remote:string}> $queue
|
|
*/
|
|
private function collectUploadFilesRecursive(string $localDir, string $remoteDir, array &$queue): void
|
|
{
|
|
$entries = @scandir($localDir);
|
|
if ($entries === false) {
|
|
return;
|
|
}
|
|
|
|
foreach ($entries as $entry) {
|
|
if ($entry === '.' || $entry === '..') {
|
|
continue;
|
|
}
|
|
|
|
$localChild = $localDir . DIRECTORY_SEPARATOR . $entry;
|
|
$remoteChild = rtrim($remoteDir, '/') . '/' . $entry;
|
|
|
|
if (is_dir($localChild)) {
|
|
$this->collectUploadFilesRecursive($localChild, $remoteChild, $queue);
|
|
} elseif (is_file($localChild) && is_readable($localChild)) {
|
|
$queue[] = [
|
|
'local' => $localChild,
|
|
'remote' => $remoteChild,
|
|
'size' => is_file($localChild) ? (int) @filesize($localChild) : 0,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Nächste Datei aus der Upload-Queue hochladen und ProgressBar aktualisieren.
|
|
*/
|
|
private function startNextUploadTask(
|
|
string &$currentPrivateKeyPath,
|
|
ServerListTab $serverListTab,
|
|
Label $statusLabel,
|
|
): void {
|
|
if ($this->totalUploadFiles <= 0) {
|
|
$this->transferProgressBar->setValue(1.0);
|
|
$this->transferInfoLabel->setText('Keine Dateien zum Hochladen');
|
|
return;
|
|
}
|
|
|
|
if (empty($this->pendingUploadQueue)) {
|
|
// Alle Dateien hochgeladen
|
|
$this->transferProgressBar->setValue(1.0);
|
|
$this->transferInfoLabel->setText('Upload abgeschlossen');
|
|
|
|
$selectedServerRef = &$serverListTab->selectedServer;
|
|
$this->reloadCurrentDirectory($currentPrivateKeyPath, $selectedServerRef, $statusLabel);
|
|
return;
|
|
}
|
|
|
|
$item = array_shift($this->pendingUploadQueue);
|
|
$localPath = $item['local'];
|
|
$remotePath = $item['remote'];
|
|
$fileSize = (int) ($item['size'] ?? 0);
|
|
|
|
$selectedServerRef = &$serverListTab->selectedServer;
|
|
$sftpTab = $this;
|
|
|
|
$uploadAsyncButton = new Button('UploadChunk', '');
|
|
$uploadAsyncButton->setOnClickAsync(
|
|
function () use (&$currentPrivateKeyPath, &$selectedServerRef, $localPath, $remotePath) {
|
|
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
|
|
return ['error' => 'Not connected'];
|
|
}
|
|
|
|
$selectedServer = $selectedServerRef;
|
|
|
|
try {
|
|
$sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']);
|
|
$key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath));
|
|
|
|
if (!$sftp->login('root', $key)) {
|
|
return ['error' => 'SFTP Login failed'];
|
|
}
|
|
|
|
// Zielverzeichnis sicherstellen
|
|
$remoteDir = dirname($remotePath);
|
|
if (!$sftp->is_dir($remoteDir)) {
|
|
if (!$sftp->mkdir($remoteDir, -1, true)) {
|
|
return ['error' => 'Kann Remote-Verzeichnis nicht anlegen'];
|
|
}
|
|
}
|
|
|
|
if (!is_file($localPath) || !is_readable($localPath)) {
|
|
return ['error' => 'Lokale Datei ist nicht lesbar'];
|
|
}
|
|
|
|
$result = $sftp->put(
|
|
$remotePath,
|
|
$localPath,
|
|
\phpseclib3\Net\SFTP::SOURCE_LOCAL_FILE,
|
|
);
|
|
if ($result === false) {
|
|
return ['error' => 'Upload fehlgeschlagen'];
|
|
}
|
|
|
|
return ['success' => true];
|
|
} catch (\Exception $e) {
|
|
return ['error' => $e->getMessage()];
|
|
}
|
|
},
|
|
function ($result) use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel, $fileSize) {
|
|
if (isset($result['error'])) {
|
|
$sftpTab->transferInfoLabel->setText('Upload fehlgeschlagen: ' . $result['error']);
|
|
$statusLabel->setText('Fehler beim Hochladen: ' . $result['error']);
|
|
return;
|
|
}
|
|
|
|
if (isset($result['success'])) {
|
|
$sftpTab->completedUploadFiles++;
|
|
$sftpTab->completedUploadBytes += $fileSize;
|
|
|
|
$progress = 0.0;
|
|
if ($sftpTab->totalUploadBytes > 0) {
|
|
$progress = $sftpTab->completedUploadBytes / $sftpTab->totalUploadBytes;
|
|
} elseif ($sftpTab->totalUploadFiles > 0) {
|
|
$progress = $sftpTab->completedUploadFiles / $sftpTab->totalUploadFiles;
|
|
} else {
|
|
$progress = 1.0;
|
|
}
|
|
|
|
$sftpTab->transferProgressBar->setValue($progress);
|
|
$sftpTab->transferInfoLabel->setText(
|
|
sprintf(
|
|
'Upload: %d / %d Dateien',
|
|
$sftpTab->completedUploadFiles,
|
|
$sftpTab->totalUploadFiles,
|
|
),
|
|
);
|
|
$sftpTab->transferBytesLabel->setText(
|
|
sprintf(
|
|
'%.2f / %.2f MB',
|
|
$sftpTab->completedUploadBytes > 0
|
|
? ($sftpTab->completedUploadBytes / (1024 * 1024))
|
|
: 0,
|
|
$sftpTab->totalUploadBytes > 0
|
|
? ($sftpTab->totalUploadBytes / (1024 * 1024))
|
|
: 0,
|
|
),
|
|
);
|
|
|
|
// Nächste Datei starten
|
|
$sftpTab->startNextUploadTask($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
|
}
|
|
},
|
|
function ($error) use ($sftpTab, $statusLabel) {
|
|
$errorMsg = is_string($error) ? $error : 'Unknown error';
|
|
$sftpTab->transferInfoLabel->setText('Upload fehlgeschlagen: ' . $errorMsg);
|
|
$statusLabel->setText('Fehler beim Hochladen: ' . $errorMsg);
|
|
},
|
|
);
|
|
|
|
$uploadAsyncButton->handleMouseClick(0, 0, 0);
|
|
}
|
|
|
|
/**
|
|
* Nächste Datei aus der Download-Queue holen und Fortschritt aktualisieren.
|
|
*/
|
|
private function startNextDownloadTask(
|
|
string &$currentPrivateKeyPath,
|
|
ServerListTab $serverListTab,
|
|
Label $statusLabel,
|
|
string $localRootDir,
|
|
): void {
|
|
if ($this->totalDownloadFiles <= 0) {
|
|
$this->transferProgressBar->setValue(1.0);
|
|
$this->transferInfoLabel->setText('Keine Dateien zum Herunterladen');
|
|
return;
|
|
}
|
|
|
|
if (empty($this->pendingDownloadQueue)) {
|
|
// Alle Dateien heruntergeladen
|
|
$this->transferProgressBar->setValue(1.0);
|
|
$this->transferInfoLabel->setText('Download abgeschlossen');
|
|
$this->localFileBrowser->loadDirectory($localRootDir);
|
|
return;
|
|
}
|
|
|
|
$item = array_shift($this->pendingDownloadQueue);
|
|
$remotePath = $item['remote'];
|
|
$localPath = $item['local'];
|
|
$fileSize = (int) ($item['size'] ?? 0);
|
|
|
|
$selectedServerRef = &$serverListTab->selectedServer;
|
|
$sftpTab = $this;
|
|
|
|
$downloadAsyncButton = new Button('DownloadChunk', '');
|
|
$downloadAsyncButton->setOnClickAsync(
|
|
function () use (&$currentPrivateKeyPath, &$selectedServerRef, $remotePath, $localPath) {
|
|
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
|
|
return ['error' => 'Not connected'];
|
|
}
|
|
|
|
$selectedServer = $selectedServerRef;
|
|
|
|
try {
|
|
$sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']);
|
|
$key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath));
|
|
|
|
if (!$sftp->login('root', $key)) {
|
|
return ['error' => 'SFTP Login failed'];
|
|
}
|
|
|
|
$localDir = dirname($localPath);
|
|
if (!is_dir($localDir)) {
|
|
if (!mkdir($localDir, 0777, true) && !is_dir($localDir)) {
|
|
return ['error' => 'Lokales Zielverzeichnis kann nicht erstellt werden'];
|
|
}
|
|
}
|
|
|
|
$result = $sftp->get($remotePath, $localPath);
|
|
if ($result === false) {
|
|
return ['error' => 'Download fehlgeschlagen'];
|
|
}
|
|
|
|
return ['success' => true];
|
|
} catch (\Exception $e) {
|
|
return ['error' => $e->getMessage()];
|
|
}
|
|
},
|
|
function ($result) use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel, $localRootDir, $fileSize) {
|
|
if (isset($result['error'])) {
|
|
$sftpTab->transferInfoLabel->setText('Download fehlgeschlagen: ' . $result['error']);
|
|
$statusLabel->setText('Fehler beim Herunterladen: ' . $result['error']);
|
|
return;
|
|
}
|
|
|
|
if (isset($result['success'])) {
|
|
$sftpTab->completedDownloadFiles++;
|
|
$sftpTab->completedDownloadBytes += $fileSize;
|
|
|
|
$progress = 0.0;
|
|
if ($sftpTab->totalDownloadBytes > 0) {
|
|
$progress = $sftpTab->completedDownloadBytes / $sftpTab->totalDownloadBytes;
|
|
} elseif ($sftpTab->totalDownloadFiles > 0) {
|
|
$progress = $sftpTab->completedDownloadFiles / $sftpTab->totalDownloadFiles;
|
|
} else {
|
|
$progress = 1.0;
|
|
}
|
|
|
|
$sftpTab->transferProgressBar->setValue($progress);
|
|
$sftpTab->transferInfoLabel->setText(
|
|
sprintf(
|
|
'Download: %d / %d Dateien',
|
|
$sftpTab->completedDownloadFiles,
|
|
$sftpTab->totalDownloadFiles,
|
|
),
|
|
);
|
|
$sftpTab->transferBytesLabel->setText(
|
|
sprintf(
|
|
'%.2f / %.2f MB',
|
|
$sftpTab->completedDownloadBytes > 0
|
|
? ($sftpTab->completedDownloadBytes / (1024 * 1024))
|
|
: 0,
|
|
$sftpTab->totalDownloadBytes > 0
|
|
? ($sftpTab->totalDownloadBytes / (1024 * 1024))
|
|
: 0,
|
|
),
|
|
);
|
|
|
|
// Nächste Datei herunterladen
|
|
$sftpTab->startNextDownloadTask($currentPrivateKeyPath, $serverListTab, $statusLabel, $localRootDir);
|
|
}
|
|
},
|
|
function ($error) use ($sftpTab, $statusLabel) {
|
|
$errorMsg = is_string($error) ? $error : 'Unknown error';
|
|
$sftpTab->transferInfoLabel->setText('Download fehlgeschlagen: ' . $errorMsg);
|
|
$statusLabel->setText('Fehler beim Herunterladen: ' . $errorMsg);
|
|
},
|
|
);
|
|
|
|
$downloadAsyncButton->handleMouseClick(0, 0, 0);
|
|
}
|
|
|
|
private function handleDownload(
|
|
string &$currentPrivateKeyPath,
|
|
ServerListTab $serverListTab,
|
|
Label $statusLabel,
|
|
): void {
|
|
if ($this->currentRemoteSelection === null) {
|
|
$statusLabel->setText('Keine Remote-Datei ausgewählt');
|
|
return;
|
|
}
|
|
|
|
$remoteRow = $this->currentRemoteSelection['row'] ?? null;
|
|
$remotePath = $this->currentRemoteSelection['path'] ?? null;
|
|
|
|
if ($remoteRow === null || $remotePath === null) {
|
|
$statusLabel->setText('Ungültige Remote-Auswahl');
|
|
return;
|
|
}
|
|
|
|
if ($serverListTab->selectedServer === null) {
|
|
$statusLabel->setText('Kein Server ausgewählt');
|
|
return;
|
|
}
|
|
|
|
if (empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) {
|
|
$statusLabel->setText('Private Key Pfad nicht konfiguriert oder Datei nicht gefunden');
|
|
return;
|
|
}
|
|
|
|
$localDir = $this->localFileBrowser->getCurrentPath();
|
|
if ($localDir === '') {
|
|
$localDir = getcwd();
|
|
}
|
|
|
|
if (!is_dir($localDir) || !is_writable($localDir)) {
|
|
$statusLabel->setText('Lokales Verzeichnis ist nicht beschreibbar');
|
|
return;
|
|
}
|
|
|
|
$this->transferProgressBar->setValue(0.0);
|
|
$this->transferInfoLabel->setText('Download wird vorbereitet …');
|
|
|
|
// Async-Scan der Remote-Struktur, um eine Download-Queue aufzubauen
|
|
$selectedServerRef = &$serverListTab->selectedServer;
|
|
$sftpTab = $this;
|
|
|
|
$scanButton = new Button('ScanDownload', '');
|
|
$scanButton->setOnClickAsync(
|
|
function () use (&$currentPrivateKeyPath, &$selectedServerRef, $remotePath, $remoteRow, $localDir) {
|
|
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
|
|
return ['error' => 'Not connected'];
|
|
}
|
|
|
|
$selectedServer = $selectedServerRef;
|
|
|
|
try {
|
|
$sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']);
|
|
$key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath));
|
|
|
|
if (!$sftp->login('root', $key)) {
|
|
return ['error' => 'SFTP Login failed'];
|
|
}
|
|
|
|
$queue = [];
|
|
$files = 0;
|
|
$dirs = 0;
|
|
$totalBytes = 0;
|
|
|
|
$isDir = (bool) ($remoteRow['isDir'] ?? false);
|
|
if (!$isDir) {
|
|
$stat = $sftp->stat($remotePath);
|
|
$size = (int) ($stat['size'] ?? 0);
|
|
$queue[] = [
|
|
'remote' => $remotePath,
|
|
'local' => rtrim($localDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . basename($remotePath),
|
|
'size' => $size,
|
|
];
|
|
$files = 1;
|
|
$totalBytes = $size;
|
|
} else {
|
|
$rootLocal = rtrim($localDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . basename($remotePath);
|
|
|
|
$collect = null;
|
|
$collect = function (string $srcDir, string $dstDir) use (&$collect, $sftp, &$queue, &$files, &$dirs, &$totalBytes) {
|
|
$dirs++;
|
|
|
|
$entries = $sftp->nlist($srcDir);
|
|
if ($entries === false) {
|
|
return;
|
|
}
|
|
|
|
foreach ($entries as $entry) {
|
|
if ($entry === '.' || $entry === '..') {
|
|
continue;
|
|
}
|
|
|
|
$remoteChild = rtrim($srcDir, '/') . '/' . $entry;
|
|
$localChild = rtrim($dstDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $entry;
|
|
|
|
$stat = $sftp->stat($remoteChild);
|
|
$isDirChild = ($stat['type'] ?? 0) === 2;
|
|
|
|
if ($isDirChild) {
|
|
$collect($remoteChild, $localChild);
|
|
} else {
|
|
$size = (int) ($stat['size'] ?? 0);
|
|
$queue[] = [
|
|
'remote' => $remoteChild,
|
|
'local' => $localChild,
|
|
'size' => $size,
|
|
];
|
|
$files++;
|
|
$totalBytes += $size;
|
|
}
|
|
}
|
|
};
|
|
|
|
$collect($remotePath, $rootLocal);
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'queue' => $queue,
|
|
'files' => $files,
|
|
'dirs' => $dirs,
|
|
'localDir' => $localDir,
|
|
'totalBytes' => $totalBytes,
|
|
];
|
|
} catch (\Exception $e) {
|
|
return ['error' => $e->getMessage()];
|
|
}
|
|
},
|
|
function ($result) use ($sftpTab, $statusLabel, &$currentPrivateKeyPath, $serverListTab) {
|
|
if (isset($result['error'])) {
|
|
$statusLabel->setText('Fehler beim Vorbereiten des Downloads: ' . $result['error']);
|
|
$sftpTab->transferInfoLabel->setText('Download fehlgeschlagen');
|
|
return;
|
|
}
|
|
|
|
if (!isset($result['success']) || !$result['success']) {
|
|
$statusLabel->setText('Unbekannter Fehler beim Vorbereiten des Downloads');
|
|
$sftpTab->transferInfoLabel->setText('Download fehlgeschlagen');
|
|
return;
|
|
}
|
|
|
|
$queue = $result['queue'] ?? [];
|
|
$files = (int) ($result['files'] ?? 0);
|
|
$dirs = (int) ($result['dirs'] ?? 0);
|
|
$totalBytes = (int) ($result['totalBytes'] ?? 0);
|
|
|
|
$sftpTab->pendingDownloadQueue = $queue;
|
|
$sftpTab->totalDownloadFiles = count($queue);
|
|
$sftpTab->completedDownloadFiles = 0;
|
|
$sftpTab->totalDownloadBytes = $totalBytes;
|
|
$sftpTab->completedDownloadBytes = 0;
|
|
|
|
if ($sftpTab->totalDownloadFiles <= 0) {
|
|
$sftpTab->transferProgressBar->setValue(1.0);
|
|
$sftpTab->transferInfoLabel->setText('Keine Dateien zum Herunterladen');
|
|
$sftpTab->transferBytesLabel->setText('');
|
|
return;
|
|
}
|
|
|
|
$sftpTab->transferProgressBar->setValue(0.0);
|
|
$sftpTab->transferInfoLabel->setText(
|
|
sprintf(
|
|
'Download: %d Dateien, %d Ordner',
|
|
$files,
|
|
$dirs,
|
|
),
|
|
);
|
|
$sftpTab->transferBytesLabel->setText(
|
|
sprintf(
|
|
'0.00 / %.2f MB',
|
|
$totalBytes > 0 ? ($totalBytes / (1024 * 1024)) : 0,
|
|
),
|
|
);
|
|
|
|
$localDir = $result['localDir'] ?? '';
|
|
$sftpTab->startNextDownloadTask($currentPrivateKeyPath, $serverListTab, $statusLabel, $localDir);
|
|
},
|
|
function ($error) use ($sftpTab, $statusLabel) {
|
|
$errorMsg = is_string($error) ? $error : 'Unknown error';
|
|
$statusLabel->setText('Fehler beim Vorbereiten des Downloads: ' . $errorMsg);
|
|
$sftpTab->transferInfoLabel->setText('Download fehlgeschlagen');
|
|
},
|
|
);
|
|
|
|
$scanButton->handleMouseClick(0, 0, 0);
|
|
}
|
|
|
|
private function createRenameModal(
|
|
string &$currentPrivateKeyPath,
|
|
ServerListTab $serverListTab,
|
|
Label $statusLabel,
|
|
): void {
|
|
// Create modal content
|
|
$modalContent = new Container('flex flex-col bg-white rounded-lg shadow-lg p-4 gap-3 w-96');
|
|
|
|
// Title
|
|
$modalContent->addComponent(new Label('Datei umbenennen', 'text-lg font-bold text-black'));
|
|
|
|
// Filename input
|
|
$this->renameInput = new TextInput(
|
|
'Neuer Dateiname',
|
|
'w-full border-2 border-gray-300 rounded px-3 py-2 bg-white text-black',
|
|
);
|
|
$modalContent->addComponent($this->renameInput);
|
|
|
|
// Button container
|
|
$buttonContainer = new Container('flex flex-row justify-end gap-2');
|
|
|
|
// Cancel button
|
|
$cancelButton = new Button('Abbrechen', 'px-4 py-2 bg-gray-300 text-black rounded hover:bg-gray-400');
|
|
$sftpTab = $this;
|
|
$cancelButton->setOnClick(function () use ($sftpTab) {
|
|
$sftpTab->renameModal->setVisible(false);
|
|
});
|
|
$buttonContainer->addComponent($cancelButton);
|
|
|
|
// Rename button
|
|
$renameButton = new Button('Umbenennen', 'px-4 py-2 bg-amber-600 text-white rounded hover:bg-amber-700');
|
|
$renameButton->setOnClick(function () use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel) {
|
|
$newFilename = trim($sftpTab->renameInput->getValue());
|
|
|
|
if (empty($newFilename)) {
|
|
$statusLabel->setText('Dateiname darf nicht leer sein');
|
|
return;
|
|
}
|
|
|
|
$oldPath = $sftpTab->currentRenameFilePath;
|
|
$directory = dirname($oldPath);
|
|
$newPath = $directory . '/' . $newFilename;
|
|
|
|
// Close rename modal
|
|
$sftpTab->renameModal->setVisible(false);
|
|
|
|
// Perform rename via SFTP
|
|
$selectedServerRef = &$serverListTab->selectedServer;
|
|
|
|
$renameAsyncButton = new Button('Rename', '');
|
|
$renameAsyncButton->setOnClickAsync(
|
|
function () use ($oldPath, $newPath, &$currentPrivateKeyPath, &$selectedServerRef) {
|
|
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
|
|
return ['error' => 'Not connected'];
|
|
}
|
|
|
|
$selectedServer = $selectedServerRef;
|
|
|
|
try {
|
|
$sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']);
|
|
$key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath));
|
|
|
|
if (!$sftp->login('root', $key)) {
|
|
return ['error' => 'SFTP Login failed'];
|
|
}
|
|
|
|
$result = $sftp->rename($oldPath, $newPath);
|
|
if ($result === false) {
|
|
return ['error' => 'Could not rename file'];
|
|
}
|
|
|
|
return ['success' => true];
|
|
} catch (\Exception $e) {
|
|
return ['error' => $e->getMessage()];
|
|
}
|
|
},
|
|
function ($result) use ($sftpTab, $statusLabel, &$currentPrivateKeyPath, &$selectedServerRef) {
|
|
if (isset($result['error'])) {
|
|
$statusLabel->setText('Fehler beim Umbenennen: ' . $result['error']);
|
|
return;
|
|
}
|
|
|
|
if (isset($result['success'])) {
|
|
$statusLabel->setText('Datei erfolgreich umbenannt');
|
|
$sftpTab->reloadCurrentDirectory($currentPrivateKeyPath, $selectedServerRef, $statusLabel);
|
|
}
|
|
},
|
|
function ($error) use ($statusLabel) {
|
|
$errorMsg = is_string($error) ? $error : 'Unknown error';
|
|
$statusLabel->setText('Fehler beim Umbenennen: ' . $errorMsg);
|
|
},
|
|
);
|
|
|
|
$renameAsyncButton->handleMouseClick(0, 0, 0);
|
|
});
|
|
$buttonContainer->addComponent($renameButton);
|
|
|
|
$modalContent->addComponent($buttonContainer);
|
|
|
|
// Create modal
|
|
$this->renameModal = new Modal($modalContent);
|
|
}
|
|
|
|
private function createDeleteConfirmModal(
|
|
string &$currentPrivateKeyPath,
|
|
ServerListTab $serverListTab,
|
|
Label $statusLabel,
|
|
): void {
|
|
// Create modal content
|
|
$modalContent = new Container('flex flex-col bg-white rounded-lg shadow-lg p-4 gap-3 w-96');
|
|
|
|
// Title
|
|
$modalContent->addComponent(new Label('Datei löschen', 'text-lg font-bold text-black'));
|
|
|
|
// Confirmation message
|
|
$this->deleteConfirmLabel = new Label(
|
|
'Möchten Sie diese Datei wirklich löschen?',
|
|
'text-sm text-gray-700',
|
|
);
|
|
$modalContent->addComponent($this->deleteConfirmLabel);
|
|
|
|
// Button container
|
|
$buttonContainer = new Container('flex flex-row justify-end gap-2');
|
|
|
|
// Cancel button
|
|
$cancelButton = new Button('Abbrechen', 'px-4 py-2 bg-gray-300 text-black rounded hover:bg-gray-400');
|
|
$sftpTab = $this;
|
|
$cancelButton->setOnClick(function () use ($sftpTab) {
|
|
$sftpTab->deleteConfirmModal->setVisible(false);
|
|
});
|
|
$buttonContainer->addComponent($cancelButton);
|
|
|
|
// Delete button
|
|
$deleteButton = new Button('Löschen', 'px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700');
|
|
$deleteButton->setOnClick(function () use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel) {
|
|
$filePath = $sftpTab->currentDeleteFilePath;
|
|
|
|
// Close delete modal
|
|
$sftpTab->deleteConfirmModal->setVisible(false);
|
|
|
|
// Perform delete via SFTP
|
|
$selectedServerRef = &$serverListTab->selectedServer;
|
|
|
|
$deleteAsyncButton = new Button('Delete', '');
|
|
$deleteAsyncButton->setOnClickAsync(
|
|
function () use ($filePath, &$currentPrivateKeyPath, &$selectedServerRef) {
|
|
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
|
|
return ['error' => 'Not connected'];
|
|
}
|
|
|
|
$selectedServer = $selectedServerRef;
|
|
|
|
try {
|
|
$sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']);
|
|
$key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath));
|
|
|
|
if (!$sftp->login('root', $key)) {
|
|
return ['error' => 'SFTP Login failed'];
|
|
}
|
|
|
|
$result = $sftp->delete($filePath);
|
|
if ($result === false) {
|
|
return ['error' => 'Could not delete file'];
|
|
}
|
|
|
|
return ['success' => true];
|
|
} catch (\Exception $e) {
|
|
return ['error' => $e->getMessage()];
|
|
}
|
|
},
|
|
function ($result) use ($sftpTab, $statusLabel, &$currentPrivateKeyPath, &$selectedServerRef) {
|
|
if (isset($result['error'])) {
|
|
$statusLabel->setText('Fehler beim Löschen: ' . $result['error']);
|
|
return;
|
|
}
|
|
|
|
if (isset($result['success'])) {
|
|
$statusLabel->setText('Datei erfolgreich gelöscht');
|
|
$sftpTab->reloadCurrentDirectory($currentPrivateKeyPath, $selectedServerRef, $statusLabel);
|
|
}
|
|
},
|
|
function ($error) use ($statusLabel) {
|
|
$errorMsg = is_string($error) ? $error : 'Unknown error';
|
|
$statusLabel->setText('Fehler beim Löschen: ' . $errorMsg);
|
|
},
|
|
);
|
|
|
|
$deleteAsyncButton->handleMouseClick(0, 0, 0);
|
|
});
|
|
$buttonContainer->addComponent($deleteButton);
|
|
|
|
$modalContent->addComponent($buttonContainer);
|
|
|
|
// Create modal
|
|
$this->deleteConfirmModal = new Modal($modalContent);
|
|
}
|
|
|
|
private function handleFileRename(
|
|
string $path,
|
|
array $row,
|
|
string &$currentPrivateKeyPath,
|
|
ServerListTab $serverListTab,
|
|
Label $statusLabel,
|
|
): void {
|
|
if ($serverListTab->selectedServer === null) {
|
|
$statusLabel->setText('Kein Server ausgewählt');
|
|
return;
|
|
}
|
|
|
|
$this->currentRenameFilePath = $path;
|
|
$filename = basename($path);
|
|
$this->renameInput->setValue($filename);
|
|
$this->renameModal->setVisible(true);
|
|
}
|
|
|
|
private function handleFileDelete(
|
|
string $path,
|
|
array $row,
|
|
string &$currentPrivateKeyPath,
|
|
ServerListTab $serverListTab,
|
|
Label $statusLabel,
|
|
): void {
|
|
if ($serverListTab->selectedServer === null) {
|
|
$statusLabel->setText('Kein Server ausgewählt');
|
|
return;
|
|
}
|
|
|
|
$this->currentDeleteFilePath = $path;
|
|
$filename = basename($path);
|
|
$this->deleteConfirmLabel->setText('Möchten Sie die Datei "' . $filename . '" wirklich löschen?');
|
|
$this->deleteConfirmModal->setVisible(true);
|
|
}
|
|
}
|