Backup
This commit is contained in:
parent
37c2a1db67
commit
312eb2c4bd
@ -53,6 +53,8 @@ class HetznerService
|
||||
'status' => ($i % 3) === 0 ? 'stopped' : 'running',
|
||||
'type' => 'cx' . (11 + (($i % 4) * 10)),
|
||||
'ipv4' => sprintf('192.168.%d.%d', floor($i / 255), $i % 255),
|
||||
'docker_status' => 'pending',
|
||||
'domains' => [],
|
||||
];
|
||||
}
|
||||
return $testData;
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace ServerManager\UI;
|
||||
|
||||
use PHPNative\Async\TaskManager;
|
||||
use PHPNative\Tailwind\Data\Icon as IconName;
|
||||
use PHPNative\Ui\Widget\Button;
|
||||
use PHPNative\Ui\Widget\Container;
|
||||
@ -30,6 +31,7 @@ class ServerListTab
|
||||
private Label $detailStatus;
|
||||
private Label $detailType;
|
||||
private Label $detailIpv4;
|
||||
private Container $detailDomainsContainer;
|
||||
|
||||
public function __construct(
|
||||
string &$apiKey,
|
||||
@ -66,11 +68,12 @@ class ServerListTab
|
||||
// Table
|
||||
$this->table = new Table(style: ' flex-1');
|
||||
$this->table->setColumns([
|
||||
['key' => 'id', 'title' => 'ID', 'width' => 100],
|
||||
['key' => 'name', 'title' => 'Name', 'width' => 400],
|
||||
['key' => 'status', 'title' => 'Status', 'width' => 120],
|
||||
['key' => 'type', 'title' => 'Typ', 'width' => 120],
|
||||
['key' => 'ipv4', 'title' => 'IPv4'],
|
||||
['key' => 'id', 'title' => 'ID', 'width' => 80],
|
||||
['key' => 'name', 'title' => 'Name'],
|
||||
['key' => 'status', 'title' => 'Status', 'width' => 100],
|
||||
['key' => 'type', 'title' => 'Typ', 'width' => 80],
|
||||
['key' => 'ipv4', 'title' => 'IPv4', 'width' => 160],
|
||||
['key' => 'docker_status', 'title' => 'Docker', 'width' => 100],
|
||||
]);
|
||||
|
||||
// Load initial test data
|
||||
@ -101,6 +104,11 @@ class ServerListTab
|
||||
$detailPanel->addComponent(new Label('IPv4:', 'text-xs text-gray-500 mt-2'));
|
||||
$detailPanel->addComponent($this->detailIpv4);
|
||||
|
||||
// Domains list
|
||||
$detailPanel->addComponent(new Label('Domains:', 'text-xs text-gray-500 mt-2'));
|
||||
$this->detailDomainsContainer = new Container('flex flex-col gap-1');
|
||||
$detailPanel->addComponent($this->detailDomainsContainer);
|
||||
|
||||
// SFTP Manager Button (handler will be set by SftpManagerTab)
|
||||
$this->sftpButton = new Button(
|
||||
'SFTP Manager öffnen',
|
||||
@ -184,6 +192,9 @@ class ServerListTab
|
||||
$serverListTab->detailType->setText($row['type']);
|
||||
$serverListTab->detailIpv4->setText($row['ipv4']);
|
||||
|
||||
$domains = $row['domains'] ?? [];
|
||||
$serverListTab->updateDomainDetails(is_array($domains) ? $domains : []);
|
||||
|
||||
$serverListTab->selectedServer = $row;
|
||||
}
|
||||
});
|
||||
@ -196,13 +207,18 @@ class ServerListTab
|
||||
$serverListTab->table->setData($serverListTab->currentServerData);
|
||||
} else {
|
||||
$filteredData = array_filter($serverListTab->currentServerData, function ($row) use ($searchTerm) {
|
||||
return str_contains(strtolower($row['name']), $searchTerm);
|
||||
return (
|
||||
str_contains(strtolower($row['name']), $searchTerm) ||
|
||||
count(array_filter($row['domains'], function ($item) use ($searchTerm) {
|
||||
return str_contains(strtolower($item), $searchTerm);
|
||||
}))
|
||||
);
|
||||
});
|
||||
$serverListTab->table->setData(array_values($filteredData));
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh button - use reference to apiKey variable
|
||||
// Refresh button - use reference to apiKey & privateKey variable
|
||||
$this->refreshButton->setOnClickAsync(
|
||||
function () use (&$currentApiKey) {
|
||||
try {
|
||||
@ -220,6 +236,8 @@ class ServerListTab
|
||||
'status' => $server->status,
|
||||
'type' => $server->serverType->name,
|
||||
'ipv4' => $server->publicNet->ipv4->ip,
|
||||
'docker_status' => 'pending',
|
||||
'domains' => [],
|
||||
];
|
||||
}
|
||||
|
||||
@ -232,32 +250,147 @@ class ServerListTab
|
||||
return ['error' => 'Exception: ' . $e->getMessage()];
|
||||
}
|
||||
},
|
||||
function ($result) use ($serverListTab) {
|
||||
function ($result) use (&$serverListTab, &$currentPrivateKeyPath) {
|
||||
if (is_array($result)) {
|
||||
if (isset($result['error'])) {
|
||||
$serverListTab->statusLabel->setText('Fehler: ' . $result['error']);
|
||||
echo "Error: {$result['error']}\n";
|
||||
} elseif (isset($result['success'], $result['servers'])) {
|
||||
$serverListTab->currentServerData = $result['servers'];
|
||||
|
||||
$searchTerm = strtolower(trim($serverListTab->searchInput->getValue()));
|
||||
if (empty($searchTerm)) {
|
||||
$serverListTab->table->setData($serverListTab->currentServerData);
|
||||
} else {
|
||||
$filteredData = array_filter($serverListTab->currentServerData, function ($row) use (
|
||||
$searchTerm,
|
||||
) {
|
||||
return str_contains(strtolower($row['name']), $searchTerm);
|
||||
});
|
||||
$serverListTab->table->setData(array_values($filteredData));
|
||||
}
|
||||
// Basisdaten setzen, Docker-Status initial auf "pending"
|
||||
$serverListTab->currentServerData = array_map(static fn($row) => array_merge([
|
||||
'docker_status' => 'pending',
|
||||
'docker' => null,
|
||||
'docker_error' => null,
|
||||
], $row), $result['servers']);
|
||||
|
||||
$serverListTab->table->setData($serverListTab->currentServerData);
|
||||
$serverListTab->statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden');
|
||||
echo "Success: {$result['count']} servers loaded\n";
|
||||
|
||||
// Danach: pro Server asynchron Docker-Infos nachladen
|
||||
foreach ($serverListTab->currentServerData as $index => $row) {
|
||||
$ip = $row['ipv4'] ?? '';
|
||||
|
||||
if (empty($ip) || empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) {
|
||||
$serverListTab->currentServerData[$index]['docker_error'] = 'Kein gültiger Private-Key oder IP';
|
||||
$serverListTab->currentServerData[$index]['docker_status'] = 'error';
|
||||
continue;
|
||||
}
|
||||
|
||||
$task = TaskManager::getInstance()->runAsync(function () use (
|
||||
$ip,
|
||||
$currentPrivateKeyPath,
|
||||
$index,
|
||||
) {
|
||||
try {
|
||||
$ssh = new \phpseclib3\Net\SSH2($ip);
|
||||
$key = \phpseclib3\Crypt\PublicKeyLoader::loadPrivateKey(file_get_contents(
|
||||
$currentPrivateKeyPath,
|
||||
));
|
||||
|
||||
if (!$ssh->login('root', $key)) {
|
||||
return [
|
||||
'index' => $index,
|
||||
'docker' => null,
|
||||
'docker_error' => 'SSH Login fehlgeschlagen',
|
||||
'docker_status' => 'error',
|
||||
];
|
||||
}
|
||||
|
||||
$output = $ssh->exec('docker inspect psc-web-1');
|
||||
|
||||
if (empty($output)) {
|
||||
return [
|
||||
'index' => $index,
|
||||
'docker' => null,
|
||||
'docker_error' => 'Leere Docker-Antwort',
|
||||
'docker_status' => 'error',
|
||||
];
|
||||
}
|
||||
|
||||
$json = json_decode($output, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return [
|
||||
'index' => $index,
|
||||
'docker' => null,
|
||||
'docker_error' => 'Ungültige Docker-JSON-Antwort',
|
||||
'docker_status' => 'error',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'index' => $index,
|
||||
'docker' => $json,
|
||||
'docker_error' => null,
|
||||
'docker_status' => 'ok',
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'index' => $index,
|
||||
'docker' => null,
|
||||
'docker_error' => 'SSH-Fehler: ' . $e->getMessage(),
|
||||
'docker_status' => 'error',
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
$task->onComplete(function ($dockerResult) use (&$serverListTab) {
|
||||
if (!is_array($dockerResult) || !isset($dockerResult['index'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$i = $dockerResult['index'];
|
||||
if (!isset($serverListTab->currentServerData[$i])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (array_key_exists('docker', $dockerResult)) {
|
||||
if (isset($dockerResult['docker'][0]['Config']['Env'])) {
|
||||
$hosts = array_filter(
|
||||
$dockerResult['docker'][0]['Config']['Env'],
|
||||
function ($item) {
|
||||
if (str_starts_with($item, 'LETSENCRYPT_HOST')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
);
|
||||
$domains = explode(
|
||||
',',
|
||||
substr(array_first($hosts), strlen('LETSENCRYPT_HOST=')),
|
||||
);
|
||||
|
||||
$serverListTab->currentServerData[$i]['domains'] = $domains;
|
||||
}
|
||||
$serverListTab->currentServerData[$i]['docker'] = $dockerResult['docker'];
|
||||
$serverListTab->table->setData($serverListTab->currentServerData);
|
||||
}
|
||||
|
||||
if (array_key_exists('docker_error', $dockerResult)) {
|
||||
$serverListTab->currentServerData[$i]['docker_error'] =
|
||||
$dockerResult['docker_error'];
|
||||
}
|
||||
|
||||
if (array_key_exists('docker_status', $dockerResult)) {
|
||||
$serverListTab->currentServerData[$i]['docker_status'] =
|
||||
$dockerResult['docker_status'];
|
||||
}
|
||||
});
|
||||
|
||||
$task->onError(function ($error) use ($serverListTab, $index) {
|
||||
$errorMsg = is_object($error) && method_exists($error, 'getMessage')
|
||||
? $error->getMessage()
|
||||
: ((string) $error);
|
||||
if (isset($serverListTab->currentServerData[$index])) {
|
||||
$serverListTab->currentServerData[$index]['docker_error'] =
|
||||
'Async Fehler: ' . $errorMsg;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
function ($error) use ($serverListTab) {
|
||||
function ($error) use (&$serverListTab) {
|
||||
$errorMsg = is_object($error) && method_exists($error, 'getMessage')
|
||||
? $error->getMessage()
|
||||
: ((string) $error);
|
||||
@ -276,4 +409,39 @@ class ServerListTab
|
||||
{
|
||||
return $this->sftpButton;
|
||||
}
|
||||
|
||||
private function updateDomainDetails(array $domains): void
|
||||
{
|
||||
$this->detailDomainsContainer->clearChildren();
|
||||
|
||||
if (empty($domains)) {
|
||||
$this->detailDomainsContainer->addComponent(new Label(
|
||||
'Keine Domains gefunden',
|
||||
'text-xs text-gray-500 italic',
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
if (!is_string($domain) || trim($domain) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$domain = trim($domain);
|
||||
$button = new Button($domain, 'text-sm text-blue-600 hover:text-blue-800 underline text-left');
|
||||
|
||||
$button->setOnClick(function () use ($domain) {
|
||||
$url = $domain;
|
||||
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
|
||||
$url = 'https://' . $url;
|
||||
}
|
||||
|
||||
// Versuche, die Domain im Standardbrowser zu öffnen (Linux-Umgebung)
|
||||
$escapedUrl = escapeshellarg($url);
|
||||
exec("xdg-open {$escapedUrl} > /dev/null 2>&1 &");
|
||||
});
|
||||
|
||||
$this->detailDomainsContainer->addComponent($button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,8 @@ class SftpManagerTab
|
||||
private Modal $deleteConfirmModal;
|
||||
private string $currentDeleteFilePath = '';
|
||||
private Label $deleteConfirmLabel;
|
||||
private null|array $currentLocalSelection = null;
|
||||
private null|array $currentRemoteSelection = null;
|
||||
|
||||
public function __construct(
|
||||
string &$apiKey,
|
||||
@ -52,6 +54,15 @@ class SftpManagerTab
|
||||
);
|
||||
$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
|
||||
$remoteBrowserContainer = new Container('flex flex-col flex-1 gap-2');
|
||||
|
||||
@ -72,7 +83,22 @@ class SftpManagerTab
|
||||
$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');
|
||||
|
||||
$uploadButton = new Button(
|
||||
'Hochladen →',
|
||||
'px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700',
|
||||
);
|
||||
$downloadButton = new Button(
|
||||
'← Herunterladen',
|
||||
'px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700',
|
||||
);
|
||||
$transferContainer->addComponent($uploadButton);
|
||||
$transferContainer->addComponent($downloadButton);
|
||||
|
||||
$this->tab->addComponent($localBrowserContainer);
|
||||
$this->tab->addComponent($transferContainer);
|
||||
$this->tab->addComponent($remoteBrowserContainer);
|
||||
|
||||
// Setup remote navigation handler
|
||||
@ -93,6 +119,15 @@ class SftpManagerTab
|
||||
// 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,
|
||||
@ -138,79 +173,88 @@ class SftpManagerTab
|
||||
// Create reference to selectedServer array outside of closure
|
||||
$selectedServerRef = &$serverListTab->selectedServer;
|
||||
|
||||
$this->remoteFileBrowser->setOnFileSelect(function ($path, $row) use (
|
||||
$sftpTab,
|
||||
&$currentPrivateKeyPath,
|
||||
&$selectedServerRef,
|
||||
$statusLabel,
|
||||
) {
|
||||
if (!isset($row['isDir']) || !$row['isDir']) {
|
||||
return;
|
||||
}
|
||||
$this->remoteFileBrowser->setOnFileSelect(
|
||||
function ($path, $row) use (
|
||||
$sftpTab,
|
||||
&$currentPrivateKeyPath,
|
||||
&$selectedServerRef,
|
||||
$statusLabel,
|
||||
) {
|
||||
// Track remote selection (for downloads)
|
||||
$sftpTab->currentRemoteSelection = [
|
||||
'path' => $path,
|
||||
'row' => $row,
|
||||
];
|
||||
|
||||
$loadButton = new Button('Load', '');
|
||||
$loadButton->setOnClickAsync(
|
||||
function () use ($path, &$currentPrivateKeyPath, &$selectedServerRef) {
|
||||
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
|
||||
return ['error' => 'Not connected'];
|
||||
}
|
||||
// Only navigate when a directory is selected
|
||||
if (!isset($row['isDir']) || !$row['isDir']) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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'];
|
||||
$loadButton = new Button('Load', '');
|
||||
$loadButton->setOnClickAsync(
|
||||
function () use ($path, &$currentPrivateKeyPath, &$selectedServerRef) {
|
||||
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
|
||||
return ['error' => 'Not connected'];
|
||||
}
|
||||
|
||||
$files = $sftp->nlist($path);
|
||||
if ($files === false) {
|
||||
return ['error' => 'Cannot read directory'];
|
||||
}
|
||||
// Copy to local variable for async context
|
||||
$selectedServer = $selectedServerRef;
|
||||
|
||||
$fileList = [];
|
||||
foreach ($files as $file) {
|
||||
if ($file === '.' || $file === '..') {
|
||||
continue;
|
||||
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'];
|
||||
}
|
||||
$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,
|
||||
];
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
$loadButton->handleMouseClick(0, 0, 0);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private function setupSftpConnectionHandler(
|
||||
@ -689,6 +733,198 @@ class SftpManagerTab
|
||||
$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 (($localRow['isDir'] ?? false) === true) {
|
||||
$statusLabel->setText('Ordner-Upload wird noch nicht unterstützt');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_file($localPath) || !is_readable($localPath)) {
|
||||
$statusLabel->setText('Lokale Datei ist nicht lesbar');
|
||||
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 = '/';
|
||||
}
|
||||
|
||||
$remotePath = rtrim($remoteDir, '/') . '/' . basename($localPath);
|
||||
|
||||
// Use reference to selected server for async operation
|
||||
$selectedServerRef = &$serverListTab->selectedServer;
|
||||
$sftpTab = $this;
|
||||
|
||||
$uploadAsyncButton = new Button('Upload', '');
|
||||
$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'];
|
||||
}
|
||||
|
||||
$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, $statusLabel, &$currentPrivateKeyPath, &$selectedServerRef) {
|
||||
if (isset($result['error'])) {
|
||||
$statusLabel->setText('Fehler beim Hochladen: ' . $result['error']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($result['success'])) {
|
||||
$statusLabel->setText('Datei erfolgreich hochgeladen');
|
||||
$sftpTab->reloadCurrentDirectory($currentPrivateKeyPath, $selectedServerRef, $statusLabel);
|
||||
}
|
||||
},
|
||||
function ($error) use ($statusLabel) {
|
||||
$errorMsg = is_string($error) ? $error : 'Unknown error';
|
||||
$statusLabel->setText('Fehler beim Hochladen: ' . $errorMsg);
|
||||
},
|
||||
);
|
||||
|
||||
$uploadAsyncButton->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 (($remoteRow['isDir'] ?? false) === true) {
|
||||
$statusLabel->setText('Ordner-Download wird noch nicht unterstützt');
|
||||
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;
|
||||
}
|
||||
|
||||
$localPath = rtrim($localDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . basename($remotePath);
|
||||
|
||||
// Use reference to selected server for async operation
|
||||
$selectedServerRef = &$serverListTab->selectedServer;
|
||||
$sftpTab = $this;
|
||||
|
||||
$downloadAsyncButton = new Button('Download', '');
|
||||
$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'];
|
||||
}
|
||||
|
||||
$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, $statusLabel, $localDir) {
|
||||
if (isset($result['error'])) {
|
||||
$statusLabel->setText('Fehler beim Herunterladen: ' . $result['error']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($result['success'])) {
|
||||
$statusLabel->setText('Datei erfolgreich heruntergeladen');
|
||||
$sftpTab->localFileBrowser->loadDirectory($localDir);
|
||||
}
|
||||
},
|
||||
function ($error) use ($statusLabel) {
|
||||
$errorMsg = is_string($error) ? $error : 'Unknown error';
|
||||
$statusLabel->setText('Fehler beim Herunterladen: ' . $errorMsg);
|
||||
},
|
||||
);
|
||||
|
||||
$downloadAsyncButton->handleMouseClick(0, 0, 0);
|
||||
}
|
||||
|
||||
private function createRenameModal(
|
||||
string &$currentPrivateKeyPath,
|
||||
ServerListTab $serverListTab,
|
||||
@ -922,4 +1158,3 @@ class SftpManagerTab
|
||||
$this->deleteConfirmModal->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,13 +2,7 @@
|
||||
{
|
||||
"id": "board_691661d89de624.46726087",
|
||||
"title": "Backlog",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "task_691661d89de735.41071535",
|
||||
"title": "Idee sammeln",
|
||||
"note": ""
|
||||
}
|
||||
]
|
||||
"tasks": []
|
||||
},
|
||||
{
|
||||
"id": "board_691661d89de806.79123800",
|
||||
@ -18,6 +12,11 @@
|
||||
"id": "task_691661d89de858.46479539",
|
||||
"title": "API anbinden",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"id": "task_691661d89de735.41071535",
|
||||
"title": "Idee sammeln",
|
||||
"note": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -75,7 +75,7 @@ class TextRenderer
|
||||
|
||||
private function loadFont(int $size): mixed
|
||||
{
|
||||
$size = max(1, (int) round($size));
|
||||
$size = max(1, (int) round($size)) * $this->pixelRatio;
|
||||
|
||||
if (isset($this->fonts[$size])) {
|
||||
return $this->fonts[$size];
|
||||
@ -199,6 +199,7 @@ class TextRenderer
|
||||
'w' => $textSize['w'],
|
||||
'h' => $textSize['h'],
|
||||
]);
|
||||
|
||||
// Note: Texture and surface are automatically cleaned up by PHP resource destructors
|
||||
}
|
||||
|
||||
|
||||
@ -344,19 +344,6 @@ abstract class Component
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional: handle drag (mouse move with pressed button).
|
||||
* Default: propagate to children.
|
||||
*/
|
||||
public function handleDrag(float $mouseX, float $mouseY): void
|
||||
{
|
||||
foreach ($this->children as $child) {
|
||||
if (method_exists($child, 'handleDrag')) {
|
||||
$child->handleDrag($mouseX, $mouseY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function layout(null|TextRenderer $textRenderer = null): void
|
||||
{
|
||||
$this->normalStylesCached = StyleParser::parse($this->style)->getValidStyles(
|
||||
@ -415,11 +402,12 @@ abstract class Component
|
||||
Profiler::increment('texture_cache_hit');
|
||||
Profiler::start('render_cached');
|
||||
// Render cached texture
|
||||
|
||||
sdl_render_texture($renderer, $texture, [
|
||||
'x' => $this->viewport->x * $this->viewport->uiScale,
|
||||
'y' => $this->viewport->y * $this->viewport->uiScale,
|
||||
'w' => $this->viewport->width * $this->viewport->uiScale,
|
||||
'h' => $this->viewport->height * $this->viewport->uiScale,
|
||||
'x' => $this->viewport->x,
|
||||
'y' => $this->viewport->y,
|
||||
'w' => $this->viewport->width,
|
||||
'h' => $this->viewport->height,
|
||||
]);
|
||||
Profiler::end('render_cached');
|
||||
Profiler::end('render');
|
||||
@ -458,12 +446,13 @@ abstract class Component
|
||||
// SDL3: sdl_rounded_box_ex uses (x1, y1, x2, y2) instead of (x, y, w, h)
|
||||
$x2 = $this->viewport->x + $this->viewport->width;
|
||||
$y2 = $this->viewport->y + $this->viewport->height;
|
||||
|
||||
sdl_rounded_box_ex(
|
||||
$renderer,
|
||||
((int) $this->viewport->x) * $this->viewport->uiScale,
|
||||
((int) $this->viewport->y) * $this->viewport->uiScale,
|
||||
((int) $x2) * $this->viewport->uiScale,
|
||||
((int) $y2) * $this->viewport->uiScale,
|
||||
(int) $this->viewport->x,
|
||||
(int) $this->viewport->y,
|
||||
(int) $x2,
|
||||
(int) $y2,
|
||||
$border->roundTopLeft ?? 0,
|
||||
$border->roundTopRight ?? 0,
|
||||
$border->roundBottomRight ?? 0,
|
||||
@ -475,20 +464,20 @@ abstract class Component
|
||||
);
|
||||
} else {
|
||||
sdl_render_fill_rect($renderer, [
|
||||
'x' => $this->viewport->x * $this->viewport->uiScale,
|
||||
'y' => $this->viewport->y * $this->viewport->uiScale,
|
||||
'w' => $this->viewport->width * $this->viewport->uiScale,
|
||||
'h' => $this->viewport->height * $this->viewport->uiScale,
|
||||
'x' => $this->viewport->x,
|
||||
'y' => $this->viewport->y,
|
||||
'w' => $this->viewport->width,
|
||||
'h' => $this->viewport->height,
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (defined('DEBUG_RENDERING') && DEBUG_RENDERING) {
|
||||
sdl_set_render_draw_color($renderer, rand(0, 255), rand(0, 255), rand(0, 255), 10);
|
||||
sdl_render_rect($renderer, [
|
||||
'x' => $this->viewport->x * $this->viewport->uiScale,
|
||||
'y' => $this->viewport->y * $this->viewport->uiScale,
|
||||
'w' => $this->viewport->width * $this->viewport->uiScale,
|
||||
'h' => $this->viewport->height * $this->viewport->uiScale,
|
||||
'x' => $this->viewport->x,
|
||||
'y' => $this->viewport->y,
|
||||
'w' => $this->viewport->width,
|
||||
'h' => $this->viewport->height,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -116,6 +116,7 @@ class Container extends Component
|
||||
height: $this->contentViewport->height,
|
||||
windowWidth: $this->contentViewport->windowWidth,
|
||||
windowHeight: $this->contentViewport->windowHeight,
|
||||
uiScale: $this->contentViewport->uiScale,
|
||||
);
|
||||
|
||||
$child->setViewport($childViewport);
|
||||
@ -138,6 +139,7 @@ class Container extends Component
|
||||
height: $this->contentViewport->windowHeight,
|
||||
windowWidth: $this->contentViewport->windowWidth,
|
||||
windowHeight: $this->contentViewport->windowHeight,
|
||||
uiScale: $this->contentViewport->uiScale,
|
||||
);
|
||||
$child->setViewport($overlayViewport);
|
||||
$child->setContentViewport(clone $overlayViewport);
|
||||
@ -272,6 +274,7 @@ class Container extends Component
|
||||
height: $isRow ? $this->contentViewport->height : 9999,
|
||||
windowWidth: $this->contentViewport->windowWidth,
|
||||
windowHeight: $this->contentViewport->windowHeight,
|
||||
uiScale: $this->contentViewport->uiScale,
|
||||
);
|
||||
$child->setViewport($tempViewport);
|
||||
$child->layout($textRenderer);
|
||||
@ -329,6 +332,7 @@ class Container extends Component
|
||||
height: $childHeight,
|
||||
windowWidth: $this->contentViewport->windowWidth,
|
||||
windowHeight: $this->contentViewport->windowHeight,
|
||||
uiScale: $this->contentViewport->uiScale,
|
||||
);
|
||||
$currentPosition += $size;
|
||||
} else {
|
||||
@ -344,6 +348,7 @@ class Container extends Component
|
||||
height: $size,
|
||||
windowWidth: $this->contentViewport->windowWidth,
|
||||
windowHeight: $this->contentViewport->windowHeight,
|
||||
uiScale: $this->contentViewport->uiScale,
|
||||
);
|
||||
$currentPosition += $size;
|
||||
}
|
||||
@ -357,7 +362,7 @@ class Container extends Component
|
||||
private function calculateSize(Width|Height|Basis $style, float $availableSpace): float
|
||||
{
|
||||
return match ($style->unit) {
|
||||
Unit::Pixel => (float) $style->value,
|
||||
Unit::Pixel => ((float) $style->value) * $this->contentViewport->uiScale,
|
||||
Unit::Point => (float) $style->value,
|
||||
Unit::Percent => ($availableSpace * $style->value) / 100,
|
||||
};
|
||||
|
||||
@ -75,8 +75,8 @@ class Icon extends Component
|
||||
if ($font) {
|
||||
$dimensions = ttf_size_text($font, $this->glyph);
|
||||
if ($dimensions !== false) {
|
||||
$width = (int) $dimensions['w'];
|
||||
$height = (int) $dimensions['h'];
|
||||
$width = ((int) $dimensions['w']) * $this->viewport->uiScale;
|
||||
$height = ((int) $dimensions['h']) * $this->viewport->uiScale;
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,7 +130,7 @@ class Icon extends Component
|
||||
{
|
||||
$this->clearTexture();
|
||||
|
||||
$font = IconFontRegistry::getFont($this->size, $this->fontPath);
|
||||
$font = IconFontRegistry::getFont($this->size * $this->viewport->uiScale, $this->fontPath);
|
||||
if (!$font) {
|
||||
$this->renderDirty = false;
|
||||
return;
|
||||
|
||||
@ -16,7 +16,6 @@ class Window
|
||||
private float $mouseY = 0;
|
||||
private Viewport $viewport;
|
||||
private bool $shouldBeReLayouted = true;
|
||||
private float $pixelRatio = 1.0;
|
||||
private float $uiScale = 1.0;
|
||||
private bool $shouldClose = false;
|
||||
private $onResize = null;
|
||||
@ -24,6 +23,7 @@ class Window
|
||||
private float $lastFpsUpdate = 0.0;
|
||||
private int $frameCounter = 0;
|
||||
private float $currentFps = 0.0;
|
||||
private bool $leftButtonDown = false;
|
||||
|
||||
public function __construct(
|
||||
private string $title,
|
||||
@ -58,11 +58,10 @@ class Window
|
||||
// Get window ID for event routing
|
||||
$this->windowId = sdl_get_window_id($this->window);
|
||||
// Use display scale as UI scale (e.g. 2.0 on HiDPI)
|
||||
if (function_exists('sdl_get_window_display_scale')) {
|
||||
$scale = sdl_get_window_display_scale($this->window);
|
||||
if ($scale > 0.1 && $scale <= 4.0) {
|
||||
$this->uiScale = (float) $scale;
|
||||
}
|
||||
$scale = sdl_get_window_display_scale($this->window);
|
||||
|
||||
if ($scale > 0.1 && $scale <= 4.0) {
|
||||
$this->uiScale = (float) $scale;
|
||||
}
|
||||
// Enable text input for this window
|
||||
sdl_start_text_input($this->window);
|
||||
@ -78,77 +77,21 @@ class Window
|
||||
if (!$this->textRenderer->init()) {
|
||||
error_log('Warning: Failed to initialize text renderer. Text rendering will not be available.');
|
||||
}
|
||||
|
||||
$this->textRenderer->setPixelRatio($this->uiScale);
|
||||
// Get actual window size
|
||||
$size = sdl_get_window_size($this->window);
|
||||
$this->width = $size[0];
|
||||
$this->height = $size[1];
|
||||
$this->viewport = new Viewport(
|
||||
windowWidth: $this->width,
|
||||
windowHeight: $this->height,
|
||||
width: $this->width,
|
||||
height: $this->height,
|
||||
windowWidth: $this->width * $this->uiScale,
|
||||
windowHeight: $this->height * $this->uiScale,
|
||||
width: $this->width * $this->uiScale,
|
||||
height: $this->height * $this->uiScale,
|
||||
uiScale: $this->uiScale,
|
||||
);
|
||||
$this->updatePixelRatio();
|
||||
|
||||
$this->lastFpsUpdate = microtime(true);
|
||||
}
|
||||
|
||||
private function updatePixelRatio(): void
|
||||
{
|
||||
$this->pixelRatio = 1.0;
|
||||
|
||||
// HiDPI‑Scaling ist optional und wird nur aktiviert,
|
||||
// wenn die Umgebungsvariable PHPNATIVE_ENABLE_HIDPI=1 gesetzt ist.
|
||||
$enableHiDpi = getenv('PHPNATIVE_ENABLE_HIDPI');
|
||||
if ($enableHiDpi !== '1') {
|
||||
if ($this->textRenderer) {
|
||||
$this->textRenderer->setPixelRatio($this->pixelRatio);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!function_exists('sdl_get_window_size') || !$this->window) {
|
||||
return;
|
||||
}
|
||||
|
||||
$windowSize = sdl_get_window_size($this->window);
|
||||
$pixelSize = null;
|
||||
|
||||
if (function_exists('sdl_get_window_size_in_pixels')) {
|
||||
$pixelSize = sdl_get_window_size_in_pixels($this->window);
|
||||
}
|
||||
|
||||
if ((!is_array($pixelSize) || count($pixelSize) < 2) && function_exists('sdl_get_renderer_output_size')) {
|
||||
$pixelSize = sdl_get_renderer_output_size($this->renderer);
|
||||
}
|
||||
|
||||
if (is_array($windowSize) && is_array($pixelSize)) {
|
||||
$logicalWidth = max(1, (int) ($windowSize[0] ?? 1));
|
||||
$logicalHeight = max(1, (int) ($windowSize[1] ?? 1));
|
||||
$pixelWidth = max(1, (int) ($pixelSize[0] ?? 1));
|
||||
$pixelHeight = max(1, (int) ($pixelSize[1] ?? 1));
|
||||
|
||||
$ratioX = $pixelWidth / $logicalWidth;
|
||||
$ratioY = $pixelHeight / $logicalHeight;
|
||||
|
||||
$computed = max($ratioX, $ratioY);
|
||||
if ($computed > 0) {
|
||||
$this->pixelRatio = max(1.0, $computed);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->textRenderer) {
|
||||
$this->textRenderer->setPixelRatio($this->pixelRatio);
|
||||
}
|
||||
}
|
||||
|
||||
public function getPixelRatio(): float
|
||||
{
|
||||
return $this->pixelRatio;
|
||||
}
|
||||
|
||||
public function getUiScale(): float
|
||||
{
|
||||
return $this->uiScale;
|
||||
@ -277,13 +220,12 @@ class Window
|
||||
if ($this->textRenderer && $this->textRenderer->isInitialized()) {
|
||||
$this->textRenderer->updateFramebuffer($newWidth, $newHeight);
|
||||
}
|
||||
$this->updatePixelRatio();
|
||||
$this->viewport->x = 0;
|
||||
$this->viewport->y = 0;
|
||||
$this->viewport->windowWidth = $newWidth;
|
||||
$this->viewport->width = $newWidth;
|
||||
$this->viewport->height = $newHeight;
|
||||
$this->viewport->windowHeight = $newHeight;
|
||||
$this->viewport->windowWidth = $newWidth * $this->viewport->uiScale;
|
||||
$this->viewport->width = $newWidth * $this->viewport->uiScale;
|
||||
$this->viewport->height = $newHeight * $this->viewport->uiScale;
|
||||
$this->viewport->windowHeight = $newHeight * $this->viewport->uiScale;
|
||||
$this->shouldBeReLayouted = true;
|
||||
if ($this->onResize) {
|
||||
($this->onResize)($this);
|
||||
@ -292,8 +234,8 @@ class Window
|
||||
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
// Convert physical pixels to logical coordinates using uiScale
|
||||
$newMouseX = (float) ($event['x'] ?? 0) / $this->uiScale;
|
||||
$newMouseY = (float) ($event['y'] ?? 0) / $this->uiScale;
|
||||
$newMouseX = ((float) ($event['x'] ?? 0)) * $this->uiScale;
|
||||
$newMouseY = ((float) ($event['y'] ?? 0)) * $this->uiScale;
|
||||
|
||||
$this->mouseX = $newMouseX;
|
||||
$this->mouseY = $newMouseY;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user