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