Fix dropped folder uploads
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
PLUGIN="u-navigator"
|
PLUGIN="u-navigator"
|
||||||
VERSION="2026.06.23.r043"
|
VERSION="2026.06.23.r044"
|
||||||
PKG_DIR="packages"
|
PKG_DIR="packages"
|
||||||
WORK_DIR=".plugin-build"
|
WORK_DIR=".plugin-build"
|
||||||
PKG_NAME="${PLUGIN}-${VERSION}.tgz"
|
PKG_NAME="${PLUGIN}-${VERSION}.tgz"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -8,11 +8,16 @@ if (!is_dir($target['path'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$uploaded = [];
|
$uploaded = [];
|
||||||
foreach ($_FILES['files']['name'] ?? [] as $index => $name) {
|
$names = unav_upload_array($_FILES['files']['name'] ?? []);
|
||||||
if ($_FILES['files']['error'][$index] !== UPLOAD_ERR_OK) {
|
$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');
|
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);
|
$relativeParent = dirname($safeName);
|
||||||
if ($relativeParent !== '.') {
|
if ($relativeParent !== '.') {
|
||||||
$parentCandidate = $target['path'] . '/' . $relativeParent;
|
$parentCandidate = $target['path'] . '/' . $relativeParent;
|
||||||
@@ -28,7 +33,7 @@ foreach ($_FILES['files']['name'] ?? [] as $index => $name) {
|
|||||||
if (!is_dir(dirname($destination['path']))) {
|
if (!is_dir(dirname($destination['path']))) {
|
||||||
unav_error(500, 'Could not create upload directory');
|
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');
|
unav_error(500, 'Could not store uploaded file');
|
||||||
}
|
}
|
||||||
$uploaded[] = [
|
$uploaded[] = [
|
||||||
@@ -39,6 +44,16 @@ foreach ($_FILES['files']['name'] ?? [] as $index => $name) {
|
|||||||
|
|
||||||
unav_json(['uploaded' => $uploaded], 201);
|
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 {
|
function unav_upload_safe_relative_path(string $name): string {
|
||||||
$parts = [];
|
$parts = [];
|
||||||
foreach (preg_split('#[/\\\\]+#', $name) ?: [] as $part) {
|
foreach (preg_split('#[/\\\\]+#', $name) ?: [] as $part) {
|
||||||
|
|||||||
+78
-6
@@ -879,19 +879,19 @@ async function loadExplorer(win, targetPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDrop(event, win, targetPath) {
|
async function handleDrop(event, win, targetPath) {
|
||||||
|
const uploadItems = await getDroppedUploadItems(event);
|
||||||
recordDebug('html.drop', {
|
recordDebug('html.drop', {
|
||||||
targetPath,
|
targetPath,
|
||||||
types: dataTransferTypes(event),
|
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) {
|
if (state.config.readOnly) {
|
||||||
showError(new Error('Read-only mode ist aktiv.'));
|
showError(new Error('Read-only mode ist aktiv.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = getDroppedFiles(event);
|
if (uploadItems.length) {
|
||||||
if (files.length) {
|
await uploadFileList(win, uploadItems, targetPath);
|
||||||
await uploadFileList(win, files, targetPath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,8 +902,10 @@ async function uploadFileList(win, files, targetPath) {
|
|||||||
}
|
}
|
||||||
if (!files.length) return;
|
if (!files.length) return;
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
for (const file of files) {
|
const uploadItems = normalizeUploadItems(files);
|
||||||
form.append('files', file, file.webkitRelativePath || file.name);
|
for (const item of uploadItems) {
|
||||||
|
form.append('files[]', item.file, item.file.name);
|
||||||
|
form.append('paths[]', item.path);
|
||||||
}
|
}
|
||||||
appendCsrf(form);
|
appendCsrf(form);
|
||||||
await fetchChecked(apiUrl(`upload.php?path=${encodeURIComponent(targetPath)}`), {
|
await fetchChecked(apiUrl(`upload.php?path=${encodeURIComponent(targetPath)}`), {
|
||||||
@@ -913,6 +915,19 @@ async function uploadFileList(win, files, targetPath) {
|
|||||||
await loadExplorer(win, 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 = {}) {
|
async function createJob(type, source, destination = null, refreshWindowIds = [], extra = {}) {
|
||||||
if (['move', 'copy', 'permissions'].includes(type) && isReadOnly()) {
|
if (['move', 'copy', 'permissions'].includes(type) && isReadOnly()) {
|
||||||
showError(new Error('Read-only mode ist aktiv.'));
|
showError(new Error('Read-only mode ist aktiv.'));
|
||||||
@@ -976,6 +991,63 @@ function hasDropPayload(event) {
|
|||||||
return types.includes('Files') || getDroppedFiles(event).length > 0;
|
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) {
|
function showError(error) {
|
||||||
const debug = recordDebug('error.shown', serializeError(error));
|
const debug = recordDebug('error.shown', serializeError(error));
|
||||||
const message = error?.message || String(error);
|
const message = error?.message || String(error);
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE PLUGIN [
|
<!DOCTYPE PLUGIN [
|
||||||
<!ENTITY name "u-navigator">
|
<!ENTITY name "u-navigator">
|
||||||
<!ENTITY author "Michael Roll">
|
<!ENTITY author "Michael Roll">
|
||||||
<!ENTITY version "2026.06.23.r043">
|
<!ENTITY version "2026.06.23.r044">
|
||||||
<!ENTITY package "&name;-&version;.tgz">
|
<!ENTITY package "&name;-&version;.tgz">
|
||||||
<!ENTITY pluginURL "https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/u-navigator.plg">
|
<!ENTITY pluginURL "https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/u-navigator.plg">
|
||||||
<!ENTITY support "https://git.casaderoll.de/michael/Unraid-Navigator">
|
<!ENTITY support "https://git.casaderoll.de/michael/Unraid-Navigator">
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<FILE Name="/boot/config/plugins/&name;/&package;">
|
<FILE Name="/boot/config/plugins/&name;/&package;">
|
||||||
<URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL>
|
<URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL>
|
||||||
<MD5>1228bd58fd43942593fe8a6c383f427a</MD5>
|
<MD5>3d681de8838b4c51cbade67d1ad0cb57</MD5>
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Run="/bin/bash">
|
<FILE Run="/bin/bash">
|
||||||
|
|||||||
Reference in New Issue
Block a user