diff --git a/README.md b/README.md index ccad001..af55487 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ Aktueller Stand: Kurzentscheidung: -U-Navigator ist als Unraid-Plugin machbar. Empfohlen wird ein hybrider Aufbau aus Unraid-Plugin, lokalem Backend-Dienst und moderner Weboberflaeche. +U-Navigator ist als natives Unraid-Dynamix-Plugin umgesetzt. Die Weboberflaeche wird direkt im Unraid-WebGUI geladen; die API-Endpunkte laufen als PHP-Dateien im Plugin. Naechster sinnvoller Schritt: -Go-Backend oder gebuendelte Runtime, damit das Plugin nicht mehr von einer vorhandenen Node.js-Installation abhaengt. +Robuste Hintergrundjobs fuer grosse Copy/Move-Operationen und eine Konfigurationsseite fuer erlaubte Roots. ## Unraid Plugin installieren @@ -37,23 +37,17 @@ Oder im WebGUI: Plugins -> Install Plugin -> URL einfuegen -> Install ``` -Danach starten: +Danach oeffnen: -```sh -rc.u-navigator start +```text +Tools -> U-Navigator ``` -Status pruefen: +Es wird kein extra Webserver und kein Node.js auf Unraid benoetigt. -```sh -rc.u-navigator status -``` +## Lokal testen -Wichtig: Dieser erste Plugin-Spike benoetigt `node` auf dem Unraid-System. Falls `rc.u-navigator start` meldet, dass Node.js fehlt, muss als naechster Schritt das Backend als Go-Binary gebaut oder Node.js separat bereitgestellt werden. - -## Lokal starten - -Der Prototyp nutzt standardmaessig `/mnt/user`. Auf Entwicklungsmaschinen ohne diesen Pfad kann ein Root gesetzt werden: +Der historische Node-Prototyp ist fuer lokale API-Tests noch im Repo. Auf Entwicklungsmaschinen ohne `/mnt/user` kann ein Root gesetzt werden: ```sh U_NAV_ROOT="$PWD" PORT=8088 npm start diff --git a/build-plugin.sh b/build-plugin.sh index 06004de..e6ad079 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,20 +2,17 @@ set -eu PLUGIN="u-navigator" -VERSION="0.1.0" +VERSION="0.2.0" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" rm -rf "$WORK_DIR" -mkdir -p "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/app" "$PKG_DIR" +mkdir -p "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets" "$PKG_DIR" cp -R plugin-root/usr "$WORK_DIR/" -cp package.json "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/app/" -cp -R public "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/app/" -cp -R server "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/app/" -find "$WORK_DIR" -name "*.test.js" -delete -chmod +x "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/scripts/rc.u-navigator" +cp public/app.js "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/app.js" +cp public/styles.css "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/styles.css" tar -czf "$PKG_DIR/$PKG_NAME" -C "$WORK_DIR" usr rm -rf "$WORK_DIR" diff --git a/packages/u-navigator-0.1.0.tgz b/packages/u-navigator-0.1.0.tgz deleted file mode 100644 index 963e76d..0000000 Binary files a/packages/u-navigator-0.1.0.tgz and /dev/null differ diff --git a/packages/u-navigator-0.2.0.tgz b/packages/u-navigator-0.2.0.tgz new file mode 100644 index 0000000..1baa3d7 Binary files /dev/null and b/packages/u-navigator-0.2.0.tgz differ diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page b/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page index c4293d0..f87382c 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page @@ -3,37 +3,24 @@ Title="U-Navigator" Icon="folder-open" --- - -
-

- U-Navigator laeuft als lokaler Dienst auf Port . - Falls der Rahmen leer bleibt, pruefe den Dienst mit rc.u-navigator status. -

- + +
+
+
+

U-Navigator

+

Initialisiere...

+
+ +
+
+ diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php new file mode 100644 index 0000000..1a78d5e --- /dev/null +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php @@ -0,0 +1,132 @@ + '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'); + } +} diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/config.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/config.php new file mode 100644 index 0000000..9bab61b --- /dev/null +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/config.php @@ -0,0 +1,7 @@ + unav_read_only(), + 'roots' => unav_roots(), +]); diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php new file mode 100644 index 0000000..5915c84 --- /dev/null +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php @@ -0,0 +1,44 @@ + $_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); diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/list.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/list.php new file mode 100644 index 0000000..c1b9180 --- /dev/null +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/list.php @@ -0,0 +1,34 @@ + $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, +]); diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/upload.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/upload.php new file mode 100644 index 0000000..f22f4e5 --- /dev/null +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/upload.php @@ -0,0 +1,29 @@ + $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); diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/scripts/rc.u-navigator b/plugin-root/usr/local/emhttp/plugins/u-navigator/scripts/rc.u-navigator deleted file mode 100644 index 91996bd..0000000 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/scripts/rc.u-navigator +++ /dev/null @@ -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 diff --git a/public/app.js b/public/app.js index 20173c1..519cec2 100644 --- a/public/app.js +++ b/public/app.js @@ -1,5 +1,6 @@ const workspace = document.querySelector('#workspace'); const statusText = document.querySelector('#statusText'); +const API_BASE = window.UNAV_API_BASE || '/api'; const state = { config: null, windows: [], @@ -25,7 +26,7 @@ init(); async function init() { try { - state.config = await api('/api/config'); + state.config = await api(apiUrl('config')); statusText.textContent = `${state.config.roots.length} Root, ${state.config.readOnly ? 'Read-only' : 'Schreibzugriff aktiv'}`; openExplorer(state.config.roots[0].path); openProperties(); @@ -276,7 +277,7 @@ async function loadExplorer(win, targetPath) { win.data.path = targetPath; render(); try { - const result = await api(`/api/list?path=${encodeURIComponent(targetPath)}`); + const result = await api(apiUrl(`list.php?path=${encodeURIComponent(targetPath)}`)); win.data.path = result.path; win.data.entries = result.entries; } catch (error) { @@ -307,7 +308,7 @@ async function handleDrop(event, win, targetPath) { for (const file of event.dataTransfer.files) { form.append('files', file, file.name); } - await fetchChecked(`/api/upload?path=${encodeURIComponent(targetPath)}`, { + await fetchChecked(apiUrl(`upload.php?path=${encodeURIComponent(targetPath)}`), { method: 'POST', body: form }); @@ -316,7 +317,7 @@ async function handleDrop(event, win, targetPath) { } async function createJob(type, source, destination) { - const job = await api(`/api/jobs/${type}`, { + const job = await api(apiUrl(`job.php?action=${encodeURIComponent(type)}`), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source, destination }) @@ -327,7 +328,7 @@ async function createJob(type, source, destination) { } async function pollJob(id) { - const job = await api(`/api/jobs/${id}`); + const job = await api(apiUrl(`job.php?id=${encodeURIComponent(id)}`)); state.jobs.set(id, job); render(); if (['queued', 'running', 'cancel_requested'].includes(job.status)) { @@ -446,3 +447,7 @@ function escapeAttr(value) { function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } + +function apiUrl(path) { + return `${API_BASE.replace(/\/$/, '')}/${path.replace(/^\//, '')}`; +} diff --git a/public/styles.css b/public/styles.css index e60dfd4..758e203 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,5 +1,4 @@ -:root { - color-scheme: light dark; +.u-nav { --bg: #f4f6f8; --surface: #ffffff; --surface-2: #eef2f5; @@ -8,12 +7,17 @@ --line: #cfd8e1; --accent: #e86f2d; --accent-2: #287a65; - --danger: #b52c34; --shadow: 0 18px 55px rgba(24, 34, 45, 0.22); + background: var(--bg); + color: var(--text); + display: grid; + font: 14px/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + grid-template-rows: auto 1fr; + min-height: calc(100vh - 170px); } @media (prefers-color-scheme: dark) { - :root { + .u-nav { --bg: #101418; --surface: #1b2229; --surface-2: #242e37; @@ -24,22 +28,16 @@ } } -* { +.u-nav *, +.u-nav *::before, +.u-nav *::after { box-sizing: border-box; } -body { - margin: 0; - min-height: 100vh; - background: var(--bg); - color: var(--text); - font: 14px/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -} - -button { +.u-nav button { + background: var(--surface); border: 1px solid var(--line); border-radius: 6px; - background: var(--surface); color: var(--text); cursor: pointer; font: inherit; @@ -47,17 +45,11 @@ button { padding: 0 10px; } -button:hover { +.u-nav button:hover { border-color: var(--accent); } -button.primary { - background: var(--accent); - border-color: var(--accent); - color: #fff; -} - -button.icon { +.u-nav button.icon { align-items: center; display: inline-flex; height: 30px; @@ -67,13 +59,7 @@ button.icon { width: 32px; } -.app-shell { - display: grid; - grid-template-rows: auto 1fr; - min-height: 100vh; -} - -.topbar { +.u-nav .topbar { align-items: center; background: var(--surface); border-bottom: 1px solid var(--line); @@ -83,30 +69,32 @@ button.icon { padding: 12px 16px; } -.topbar h1 { +.u-nav .topbar h1 { + color: var(--text); font-size: 18px; line-height: 1; margin: 0 0 4px; } -.topbar p { +.u-nav .topbar p, +.u-nav .muted { color: var(--muted); margin: 0; } -.toolbar { +.u-nav .toolbar { display: flex; flex-wrap: wrap; gap: 8px; } -.workspace { +.u-nav .workspace { min-height: 0; overflow: hidden; position: relative; } -.window { +.u-nav .window { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; @@ -119,15 +107,15 @@ button.icon { position: absolute; } -.window.focused { +.u-nav .window.focused { border-color: color-mix(in srgb, var(--accent) 62%, var(--line)); } -.window.maximized { +.u-nav .window.maximized { border-radius: 0; } -.titlebar { +.u-nav .titlebar { align-items: center; background: var(--surface-2); border-bottom: 1px solid var(--line); @@ -140,24 +128,24 @@ button.icon { user-select: none; } -.titlebar strong { +.u-nav .titlebar strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.window-actions { +.u-nav .window-actions { display: flex; gap: 6px; } -.window-body { +.u-nav .window-body { min-height: 0; overflow: auto; padding: 10px; } -.resize-handle { +.u-nav .resize-handle { bottom: 0; cursor: nwse-resize; height: 18px; @@ -166,7 +154,7 @@ button.icon { width: 18px; } -.resize-handle::after { +.u-nav .resize-handle::after { border-bottom: 2px solid var(--muted); border-right: 2px solid var(--muted); bottom: 5px; @@ -177,7 +165,7 @@ button.icon { width: 8px; } -.pathbar { +.u-nav .pathbar { align-items: center; display: grid; gap: 8px; @@ -185,7 +173,7 @@ button.icon { margin-bottom: 10px; } -.pathbar input { +.u-nav .pathbar input { background: var(--surface-2); border: 1px solid var(--line); border-radius: 6px; @@ -196,53 +184,53 @@ button.icon { width: 100%; } -.drop-zone { +.u-nav .drop-zone { border: 1px dashed transparent; border-radius: 8px; min-height: 180px; } -.drop-zone.drag-over { +.u-nav .drop-zone.drag-over { background: color-mix(in srgb, var(--accent) 10%, transparent); border-color: var(--accent); } -.file-table { +.u-nav .file-table { border-collapse: collapse; width: 100%; } -.file-table th, -.file-table td { +.u-nav .file-table th, +.u-nav .file-table td { border-bottom: 1px solid var(--line); padding: 8px 7px; text-align: left; vertical-align: middle; } -.file-table th { +.u-nav .file-table th { color: var(--muted); font-size: 12px; font-weight: 600; } -.file-row { +.u-nav .file-row { cursor: default; } -.file-row:hover, -.file-row.selected { +.u-nav .file-row:hover, +.u-nav .file-row.selected { background: var(--surface-2); } -.name-cell { +.u-nav .name-cell { align-items: center; display: flex; gap: 8px; min-width: 150px; } -.badge { +.u-nav .badge { border: 1px solid var(--line); border-radius: 999px; color: var(--muted); @@ -253,22 +241,18 @@ button.icon { padding: 2px 8px; } -.muted { - color: var(--muted); -} - -.queue-list { +.u-nav .queue-list { display: grid; gap: 10px; } -.job { +.u-nav .job { border: 1px solid var(--line); border-radius: 8px; padding: 10px; } -.progress { +.u-nav .progress { background: var(--surface-2); border-radius: 999px; height: 8px; @@ -276,40 +260,40 @@ button.icon { overflow: hidden; } -.progress div { +.u-nav .progress div { background: var(--accent-2); height: 100%; width: 0; } -.properties dl { +.u-nav .properties dl { display: grid; - grid-template-columns: max-content 1fr; gap: 8px 14px; + grid-template-columns: max-content 1fr; margin: 0; } -.properties dt { +.u-nav .properties dt { color: var(--muted); } -.properties dd { +.u-nav .properties dd { margin: 0; overflow-wrap: anywhere; } @media (max-width: 720px) { - .topbar { + .u-nav .topbar { align-items: flex-start; flex-direction: column; } - .workspace { + .u-nav .workspace { overflow: auto; } - .window, - .window.maximized { + .u-nav .window, + .u-nav .window.maximized { border-left: 0; border-radius: 0; border-right: 0; @@ -321,11 +305,11 @@ button.icon { width: 100% !important; } - .window:not(.focused) { + .u-nav .window:not(.focused) { display: none; } - .resize-handle { + .u-nav .resize-handle { display: none; } } diff --git a/u-navigator.plg b/u-navigator.plg index 0a71860..af51a8c 100644 --- a/u-navigator.plg +++ b/u-navigator.plg @@ -1,37 +1,32 @@ - + ]> ### &version; -- Initial U-Navigator plugin spike. -- Installs WebGUI page, Node.js prototype backend, in-app window manager and drag-and-drop file navigator. +- Native Dynamix-embedded U-Navigator page. +- Adds PHP API endpoints for listing, upload, move and copy without a separate web server. https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package; -9d6e248cb70c803f04bf491b06520764 +47fea71317002e9ea131ca468a870c69 mkdir -p /boot/config/plugins/&name; tar -xzf /boot/config/plugins/&name;/&package; -C / -chmod +x /usr/local/emhttp/plugins/&name;/scripts/rc.u-navigator -ln -sf /usr/local/emhttp/plugins/&name;/scripts/rc.u-navigator /usr/local/sbin/rc.u-navigator +find /boot/config/plugins/&name; -type f -name "&name;-*.tgz" ! -name "&package;" -delete echo "" echo "-----------------------------------------------------------" echo " &name; &version; has been installed." echo "" -echo " Start: rc.u-navigator start" -echo " Stop: rc.u-navigator stop" -echo " Status: rc.u-navigator status" -echo "" -echo " Note: This prototype requires Node.js to be available on Unraid." +echo " Open: Tools -> U-Navigator" echo "-----------------------------------------------------------" echo "" @@ -39,7 +34,6 @@ echo "" -rc.u-navigator stop 2>/dev/null || true rm -f /usr/local/sbin/rc.u-navigator rm -rf /usr/local/emhttp/plugins/&name; rm -rf /boot/config/plugins/&name; diff --git a/u-navigator.plg.in b/u-navigator.plg.in index bf3dce8..cbffe0e 100644 --- a/u-navigator.plg.in +++ b/u-navigator.plg.in @@ -8,8 +8,8 @@ ### &version; -- Initial U-Navigator plugin spike. -- Installs WebGUI page, Node.js prototype backend, in-app window manager and drag-and-drop file navigator. +- Native Dynamix-embedded U-Navigator page. +- Adds PHP API endpoints for listing, upload, move and copy without a separate web server. @@ -21,17 +21,12 @@ mkdir -p /boot/config/plugins/&name; tar -xzf /boot/config/plugins/&name;/&package; -C / -chmod +x /usr/local/emhttp/plugins/&name;/scripts/rc.u-navigator -ln -sf /usr/local/emhttp/plugins/&name;/scripts/rc.u-navigator /usr/local/sbin/rc.u-navigator +find /boot/config/plugins/&name; -type f -name "&name;-*.tgz" ! -name "&package;" -delete echo "" echo "-----------------------------------------------------------" echo " &name; &version; has been installed." echo "" -echo " Start: rc.u-navigator start" -echo " Stop: rc.u-navigator stop" -echo " Status: rc.u-navigator status" -echo "" -echo " Note: This prototype requires Node.js to be available on Unraid." +echo " Open: Tools -> U-Navigator" echo "-----------------------------------------------------------" echo "" @@ -39,7 +34,6 @@ echo "" -rc.u-navigator stop 2>/dev/null || true rm -f /usr/local/sbin/rc.u-navigator rm -rf /usr/local/emhttp/plugins/&name; rm -rf /boot/config/plugins/&name;