diff --git a/build-plugin.sh b/build-plugin.sh
index 0dc293b..8eb7a79 100755
--- a/build-plugin.sh
+++ b/build-plugin.sh
@@ -2,7 +2,7 @@
set -eu
PLUGIN="u-navigator"
-VERSION="2026.06.23.r043"
+VERSION="2026.06.23.r044"
PKG_DIR="packages"
WORK_DIR=".plugin-build"
PKG_NAME="${PLUGIN}-${VERSION}.tgz"
diff --git a/packages/u-navigator-2026.06.23.r043.tgz b/packages/u-navigator-2026.06.23.r043.tgz
deleted file mode 100644
index f52afb9..0000000
Binary files a/packages/u-navigator-2026.06.23.r043.tgz and /dev/null differ
diff --git a/packages/u-navigator-2026.06.23.r044.tgz b/packages/u-navigator-2026.06.23.r044.tgz
new file mode 100644
index 0000000..f725761
Binary files /dev/null and b/packages/u-navigator-2026.06.23.r044.tgz differ
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
index cf81f2a..cb6ae33 100644
--- a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/upload.php
+++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/upload.php
@@ -8,11 +8,16 @@ if (!is_dir($target['path'])) {
}
$uploaded = [];
-foreach ($_FILES['files']['name'] ?? [] as $index => $name) {
- if ($_FILES['files']['error'][$index] !== UPLOAD_ERR_OK) {
+$names = unav_upload_array($_FILES['files']['name'] ?? []);
+$errors = unav_upload_array($_FILES['files']['error'] ?? []);
+$tmpNames = unav_upload_array($_FILES['files']['tmp_name'] ?? []);
+$relativePaths = unav_upload_array($_POST['paths'] ?? []);
+foreach ($names as $index => $name) {
+ if (($errors[$index] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
unav_error(400, 'Upload failed');
}
- $safeName = unav_upload_safe_relative_path((string)$name);
+ $relativeName = $relativePaths[$index] ?? $name;
+ $safeName = unav_upload_safe_relative_path((string)$relativeName);
$relativeParent = dirname($safeName);
if ($relativeParent !== '.') {
$parentCandidate = $target['path'] . '/' . $relativeParent;
@@ -28,7 +33,7 @@ foreach ($_FILES['files']['name'] ?? [] as $index => $name) {
if (!is_dir(dirname($destination['path']))) {
unav_error(500, 'Could not create upload directory');
}
- if (!move_uploaded_file($_FILES['files']['tmp_name'][$index], $destination['path'])) {
+ if (!move_uploaded_file($tmpNames[$index] ?? '', $destination['path'])) {
unav_error(500, 'Could not store uploaded file');
}
$uploaded[] = [
@@ -39,6 +44,16 @@ foreach ($_FILES['files']['name'] ?? [] as $index => $name) {
unav_json(['uploaded' => $uploaded], 201);
+function unav_upload_array($value): array {
+ if (is_array($value)) {
+ return array_values($value);
+ }
+ if ($value === null || $value === '') {
+ return [];
+ }
+ return [$value];
+}
+
function unav_upload_safe_relative_path(string $name): string {
$parts = [];
foreach (preg_split('#[/\\\\]+#', $name) ?: [] as $part) {
diff --git a/server/public/app.js b/server/public/app.js
index 6761892..1762d6b 100644
--- a/server/public/app.js
+++ b/server/public/app.js
@@ -879,19 +879,19 @@ async function loadExplorer(win, targetPath) {
}
async function handleDrop(event, win, targetPath) {
+ const uploadItems = await getDroppedUploadItems(event);
recordDebug('html.drop', {
targetPath,
types: dataTransferTypes(event),
- files: getDroppedFiles(event).map((file) => ({ name: file.name, size: file.size }))
+ files: uploadItems.map((item) => ({ name: item.path, size: item.file.size }))
});
if (state.config.readOnly) {
showError(new Error('Read-only mode ist aktiv.'));
return;
}
- const files = getDroppedFiles(event);
- if (files.length) {
- await uploadFileList(win, files, targetPath);
+ if (uploadItems.length) {
+ await uploadFileList(win, uploadItems, targetPath);
}
}
@@ -902,8 +902,10 @@ async function uploadFileList(win, files, targetPath) {
}
if (!files.length) return;
const form = new FormData();
- for (const file of files) {
- form.append('files', file, file.webkitRelativePath || file.name);
+ const uploadItems = normalizeUploadItems(files);
+ for (const item of uploadItems) {
+ form.append('files[]', item.file, item.file.name);
+ form.append('paths[]', item.path);
}
appendCsrf(form);
await fetchChecked(apiUrl(`upload.php?path=${encodeURIComponent(targetPath)}`), {
@@ -913,6 +915,19 @@ async function uploadFileList(win, files, targetPath) {
await loadExplorer(win, targetPath);
}
+function normalizeUploadItems(files) {
+ return files.map((item) => {
+ if (item?.file && item?.path) {
+ return item;
+ }
+ const file = item;
+ return {
+ file,
+ path: file.webkitRelativePath || file.name
+ };
+ }).filter((item) => item.file && item.path);
+}
+
async function createJob(type, source, destination = null, refreshWindowIds = [], extra = {}) {
if (['move', 'copy', 'permissions'].includes(type) && isReadOnly()) {
showError(new Error('Read-only mode ist aktiv.'));
@@ -976,6 +991,63 @@ function hasDropPayload(event) {
return types.includes('Files') || getDroppedFiles(event).length > 0;
}
+async function getDroppedUploadItems(event) {
+ const entries = getDroppedEntries(event);
+ if (entries.length) {
+ const items = [];
+ for (const entry of entries) {
+ items.push(...await readDroppedEntry(entry, ''));
+ }
+ if (items.length) {
+ return items;
+ }
+ }
+ return getDroppedFiles(event).filter((file) => file.size > 0 || file.type || file.name.includes('.')).map((file) => ({
+ file,
+ path: file.webkitRelativePath || file.name
+ }));
+}
+
+function getDroppedEntries(event) {
+ try {
+ return [...(event.dataTransfer?.items || [])]
+ .map((item) => item.webkitGetAsEntry?.())
+ .filter(Boolean);
+ } catch (error) {
+ recordDebug('dataTransfer.entries.error', serializeError(error));
+ return [];
+ }
+}
+
+async function readDroppedEntry(entry, parentPath) {
+ const path = parentPath ? `${parentPath}/${entry.name}` : entry.name;
+ if (entry.isFile) {
+ const file = await new Promise((resolve, reject) => entry.file(resolve, reject));
+ return [{ file, path }];
+ }
+ if (!entry.isDirectory) {
+ return [];
+ }
+ const reader = entry.createReader();
+ const children = await readAllDirectoryEntries(reader);
+ const results = [];
+ for (const child of children) {
+ results.push(...await readDroppedEntry(child, path));
+ }
+ return results;
+}
+
+async function readAllDirectoryEntries(reader) {
+ const entries = [];
+ while (true) {
+ const batch = await new Promise((resolve, reject) => reader.readEntries(resolve, reject));
+ if (!batch.length) {
+ return entries;
+ }
+ entries.push(...batch);
+ }
+}
+
function showError(error) {
const debug = recordDebug('error.shown', serializeError(error));
const message = error?.message || String(error);
diff --git a/u-navigator.plg b/u-navigator.plg
index 4742172..725abc7 100644
--- a/u-navigator.plg
+++ b/u-navigator.plg
@@ -1,7 +1,7 @@
-
+
@@ -24,7 +24,7 @@
https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;
-1228bd58fd43942593fe8a6c383f427a
+3d681de8838b4c51cbade67d1ad0cb57