Embed U-Navigator in Dynamix

This commit is contained in:
Mikei386
2026-06-22 22:21:49 +02:00
parent 92434a945d
commit ae5e192eac
15 changed files with 353 additions and 214 deletions
@@ -3,37 +3,24 @@ Title="U-Navigator"
Icon="folder-open"
---
<?PHP
$port = 8088;
$plugin = "u-navigator";
?>
<style>
.u-navigator-plugin {
display: grid;
gap: 12px;
min-height: calc(100vh - 170px);
}
.u-navigator-plugin iframe {
border: 1px solid var(--border-color, #d2d8df);
border-radius: 8px;
min-height: calc(100vh - 190px);
width: 100%;
}
.u-navigator-plugin .notice {
color: var(--text-muted, #677);
margin: 0;
}
</style>
<div class="u-navigator-plugin">
<p class="notice">
U-Navigator laeuft als lokaler Dienst auf Port <?=htmlspecialchars($port)?>.
Falls der Rahmen leer bleibt, pruefe den Dienst mit <code>rc.u-navigator status</code>.
</p>
<iframe id="u-navigator-frame" title="U-Navigator"></iframe>
<link rel="stylesheet" href="/plugins/<?=$plugin?>/assets/styles.css">
<div class="u-nav app-shell">
<header class="topbar">
<div>
<h1>U-Navigator</h1>
<p id="statusText">Initialisiere...</p>
</div>
<nav class="toolbar" aria-label="Workspace actions">
<button id="newExplorerButton" title="Explorer oeffnen">Explorer</button>
<button id="newPropertiesButton" title="Eigenschaften oeffnen">Eigenschaften</button>
<button id="newQueueButton" title="Transfer-Queue oeffnen">Transfers</button>
</nav>
</header>
<main id="workspace" class="workspace" aria-label="U-Navigator Workspace"></main>
</div>
<script>
(() => {
const frame = document.getElementById('u-navigator-frame');
frame.src = `${window.location.protocol}//${window.location.hostname}:<?=htmlspecialchars($port)?>/`;
})();
window.UNAV_API_BASE = "/plugins/<?=$plugin?>/api";
</script>
<script src="/plugins/<?=$plugin?>/assets/app.js"></script>
@@ -0,0 +1,132 @@
<?php
const UNAV_DEFAULT_ROOT = '/mnt/user';
function unav_roots(): array {
$configured = getenv('U_NAV_ROOTS') ?: UNAV_DEFAULT_ROOT;
$roots = array_values(array_filter(array_map('trim', explode(',', $configured))));
if (!$roots) {
$roots = [UNAV_DEFAULT_ROOT];
}
$resolved = [];
foreach ($roots as $root) {
$real = realpath($root);
if ($real !== false && is_dir($real)) {
$resolved[] = [
'id' => 'root' . (count($resolved) + 1),
'label' => $root,
'path' => $real,
];
}
}
if (!$resolved) {
unav_error(500, 'No usable U-Navigator root found');
}
return $resolved;
}
function unav_read_only(): bool {
return getenv('U_NAV_READ_ONLY') === '1';
}
function unav_json($payload, int $status = 200): void {
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($payload, JSON_UNESCAPED_SLASHES);
exit;
}
function unav_error(int $status, string $message): void {
unav_json(['error' => $message], $status);
}
function unav_is_inside(string $path, string $root): bool {
if ($path === $root) {
return true;
}
return str_starts_with($path, rtrim($root, '/') . '/');
}
function unav_root_for_real(string $real): array {
foreach (unav_roots() as $root) {
if (unav_is_inside($real, $root['path'])) {
return $root;
}
}
unav_error(403, 'Path is outside allowed roots');
}
function unav_existing_path(?string $input): array {
$roots = unav_roots();
$candidate = $input ?: $roots[0]['path'];
$real = realpath($candidate);
if ($real === false) {
unav_error(404, 'Path not found');
}
$root = unav_root_for_real($real);
return ['root' => $root, 'path' => $real];
}
function unav_destination_path(?string $input): array {
if (!$input) {
unav_error(400, 'Missing destination');
}
$candidate = $input;
$parent = dirname($candidate);
$parentReal = realpath($parent);
if ($parentReal === false) {
unav_error(404, 'Destination parent not found');
}
$root = unav_root_for_real($parentReal);
return ['root' => $root, 'path' => $parentReal . '/' . basename($candidate)];
}
function unav_assert_writable(): void {
if (unav_read_only()) {
unav_error(403, 'Read-only mode is enabled');
}
}
function unav_request_json(): array {
$raw = file_get_contents('php://input');
$data = json_decode($raw ?: '{}', true);
if (!is_array($data)) {
unav_error(400, 'Invalid JSON body');
}
return $data;
}
function unav_copy_recursive(string $source, string $destination): void {
if (is_link($source)) {
unav_error(400, 'Copying symlinks is not allowed');
}
if (is_dir($source)) {
if (!mkdir($destination, 0777, true) && !is_dir($destination)) {
unav_error(500, 'Could not create destination directory');
}
foreach (scandir($source) ?: [] as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
unav_copy_recursive($source . '/' . $entry, $destination . '/' . $entry);
}
return;
}
if (!is_file($source)) {
unav_error(400, 'Only files and directories can be copied');
}
if (!copy($source, $destination)) {
unav_error(500, 'Copy failed');
}
}
function unav_guard_destination(string $source, string $destination): void {
if (file_exists($destination)) {
unav_error(409, 'Destination already exists');
}
if ($source === $destination || str_starts_with($destination, rtrim($source, '/') . '/')) {
unav_error(400, 'Destination cannot be inside source');
}
}
@@ -0,0 +1,7 @@
<?php
require_once __DIR__ . '/common.php';
unav_json([
'readOnly' => unav_read_only(),
'roots' => unav_roots(),
]);
@@ -0,0 +1,44 @@
<?php
require_once __DIR__ . '/common.php';
unav_assert_writable();
$action = $_GET['action'] ?? '';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
unav_json([
'id' => $_GET['id'] ?? '0',
'type' => 'unknown',
'status' => 'completed',
'progress' => 100,
'message' => 'Completed',
'error' => null,
'createdAt' => date(DATE_ATOM),
'updatedAt' => date(DATE_ATOM),
]);
}
$data = unav_request_json();
$source = unav_existing_path($data['source'] ?? null);
$destination = unav_destination_path($data['destination'] ?? null);
unav_guard_destination($source['path'], $destination['path']);
if ($action === 'move') {
if (!rename($source['path'], $destination['path'])) {
unav_error(500, 'Move failed');
}
} elseif ($action === 'copy') {
unav_copy_recursive($source['path'], $destination['path']);
} else {
unav_error(404, 'Unknown job action');
}
unav_json([
'id' => (string)time(),
'type' => $action,
'status' => 'completed',
'progress' => 100,
'message' => 'Completed',
'error' => null,
'createdAt' => date(DATE_ATOM),
'updatedAt' => date(DATE_ATOM),
], 202);
@@ -0,0 +1,34 @@
<?php
require_once __DIR__ . '/common.php';
$resolved = unav_existing_path($_GET['path'] ?? null);
if (!is_dir($resolved['path'])) {
unav_error(400, 'Path is not a directory');
}
$entries = [];
foreach (scandir($resolved['path']) ?: [] as $name) {
if ($name === '.' || $name === '..') {
continue;
}
$full = $resolved['path'] . '/' . $name;
$type = is_link($full) ? 'symlink' : (is_dir($full) ? 'directory' : (is_file($full) ? 'file' : 'other'));
$entries[] = [
'name' => $name,
'path' => $full,
'type' => $type,
'size' => is_file($full) ? filesize($full) : 0,
'modifiedAt' => date(DATE_ATOM, filemtime($full) ?: time()),
];
}
usort($entries, function ($a, $b) {
if ($a['type'] === 'directory' && $b['type'] !== 'directory') return -1;
if ($a['type'] !== 'directory' && $b['type'] === 'directory') return 1;
return strnatcasecmp($a['name'], $b['name']);
});
unav_json([
'path' => $resolved['path'],
'entries' => $entries,
]);
@@ -0,0 +1,29 @@
<?php
require_once __DIR__ . '/common.php';
unav_assert_writable();
$target = unav_existing_path($_GET['path'] ?? null);
if (!is_dir($target['path'])) {
unav_error(400, 'Upload target must be a directory');
}
$uploaded = [];
foreach ($_FILES['files']['name'] ?? [] as $index => $name) {
if ($_FILES['files']['error'][$index] !== UPLOAD_ERR_OK) {
unav_error(400, 'Upload failed');
}
$safeName = basename($name);
$destination = unav_destination_path($target['path'] . '/' . $safeName);
if (file_exists($destination['path'])) {
unav_error(409, 'Destination already exists');
}
if (!move_uploaded_file($_FILES['files']['tmp_name'][$index], $destination['path'])) {
unav_error(500, 'Could not store uploaded file');
}
$uploaded[] = [
'name' => $safeName,
'size' => filesize($destination['path']) ?: 0,
];
}
unav_json(['uploaded' => $uploaded], 201);
@@ -1,62 +0,0 @@
#!/bin/bash
PLUGIN="u-navigator"
APP_DIR="/usr/local/emhttp/plugins/${PLUGIN}/app"
PID_FILE="/var/run/${PLUGIN}.pid"
LOG_FILE="/var/log/${PLUGIN}.log"
PORT="${U_NAV_PORT:-8088}"
ROOTS="${U_NAV_ROOTS:-/mnt/user}"
HOST="${U_NAV_HOST:-0.0.0.0}"
is_running() {
[[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null
}
start() {
if is_running; then
echo "${PLUGIN} is already running with PID $(cat "$PID_FILE")"
return 0
fi
if ! command -v node >/dev/null 2>&1; then
echo "node is required but was not found. Install/provide Node.js before starting ${PLUGIN}."
return 1
fi
mkdir -p "$(dirname "$PID_FILE")"
U_NAV_ROOTS="$ROOTS" U_NAV_HOST="$HOST" PORT="$PORT" nohup node "${APP_DIR}/server/server.js" >>"$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
echo "${PLUGIN} started on ${HOST}:${PORT} with roots ${ROOTS}"
}
stop() {
if ! is_running; then
rm -f "$PID_FILE"
echo "${PLUGIN} is not running"
return 0
fi
kill "$(cat "$PID_FILE")" 2>/dev/null || true
rm -f "$PID_FILE"
echo "${PLUGIN} stopped"
}
status() {
if is_running; then
echo "${PLUGIN} is running with PID $(cat "$PID_FILE")"
else
echo "${PLUGIN} is not running"
return 1
fi
}
case "$1" in
start) start ;;
stop) stop ;;
restart) stop; start ;;
status) status ;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 2
;;
esac