Embed U-Navigator in Dynamix
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user