diff --git a/build-plugin.sh b/build-plugin.sh
index d991b9a..f7f042b 100755
--- a/build-plugin.sh
+++ b/build-plugin.sh
@@ -2,7 +2,7 @@
set -eu
PLUGIN="u-navigator"
-VERSION="2026.06.23.r047"
+VERSION="2026.06.23.r048"
PKG_DIR="packages"
WORK_DIR=".plugin-build"
PKG_NAME="${PLUGIN}-${VERSION}.tgz"
diff --git a/packages/u-navigator-2026.06.23.r047.tgz b/packages/u-navigator-2026.06.23.r047.tgz
deleted file mode 100644
index 5d302cf..0000000
Binary files a/packages/u-navigator-2026.06.23.r047.tgz and /dev/null differ
diff --git a/packages/u-navigator-2026.06.23.r048.tgz b/packages/u-navigator-2026.06.23.r048.tgz
new file mode 100644
index 0000000..5eee467
Binary files /dev/null and b/packages/u-navigator-2026.06.23.r048.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 acc6832..a821d2d 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
@@ -7,6 +7,44 @@ if (!is_dir($target['path'])) {
unav_error(400, 'Upload target must be a directory');
}
+if (($_POST['uploadMode'] ?? '') === 'chunk') {
+ $relativeName = (string)($_POST['relativePath'] ?? '');
+ $chunkIndex = filter_var($_POST['chunkIndex'] ?? null, FILTER_VALIDATE_INT);
+ $totalChunks = filter_var($_POST['totalChunks'] ?? null, FILTER_VALIDATE_INT);
+ $encoded = (string)($_POST['data'] ?? '');
+ if ($relativeName === '' || $chunkIndex === false || $totalChunks === false || $chunkIndex < 0 || $totalChunks < 1 || $chunkIndex >= $totalChunks) {
+ unav_error(400, 'Invalid upload chunk');
+ }
+ $destination = unav_prepare_upload_destination($target['path'], $relativeName);
+ $temporary = $destination['path'] . '.u-nav-upload';
+ if ($chunkIndex === 0) {
+ @unlink($temporary);
+ } elseif (!is_file($temporary)) {
+ unav_error(400, 'Upload session not found');
+ }
+ $data = base64_decode($encoded, true);
+ if ($data === false) {
+ unav_error(400, 'Invalid upload data');
+ }
+ if (file_put_contents($temporary, $data, $chunkIndex === 0 ? LOCK_EX : FILE_APPEND | LOCK_EX) === false) {
+ unav_error(500, 'Could not write upload chunk');
+ }
+ $complete = $chunkIndex === $totalChunks - 1;
+ if ($complete && !rename($temporary, $destination['path'])) {
+ @unlink($temporary);
+ unav_error(500, 'Could not finalize upload');
+ }
+ unav_json([
+ 'uploaded' => $complete ? [[
+ 'name' => $destination['relative'],
+ 'size' => filesize($destination['path']) ?: 0,
+ ]] : [],
+ 'chunk' => $chunkIndex + 1,
+ 'totalChunks' => $totalChunks,
+ 'complete' => $complete,
+ ], $complete ? 201 : 202);
+}
+
if (isset($_GET['relativePath']) || isset($_SERVER['HTTP_X_UNAV_RELATIVE_PATH'])) {
$relativeName = (string)($_GET['relativePath'] ?? $_SERVER['HTTP_X_UNAV_RELATIVE_PATH']);
$destination = unav_prepare_upload_destination($target['path'], $relativeName);
diff --git a/server/public/app.js b/server/public/app.js
index c29d2cd..69fcb9b 100644
--- a/server/public/app.js
+++ b/server/public/app.js
@@ -904,25 +904,37 @@ async function uploadFileList(win, files, targetPath) {
const uploadItems = normalizeUploadItems(files);
for (const [index, item] of uploadItems.entries()) {
recordDebug('upload.file.request', { path: item.path, size: item.file.size, index: index + 1, total: uploadItems.length });
- const params = new URLSearchParams({
- path: targetPath,
- relativePath: item.path
- });
- const token = getCsrfToken();
- if (token) {
- params.set('csrf_token', token);
- recordDebug('csrf.attached', { length: token.length });
- } else {
- recordDebug('csrf.missing', {});
- }
- const response = await fetch(apiUrl(`upload.php?${params.toString()}`), {
+ await uploadFileChunked(targetPath, item.file, item.path);
+ recordDebug('upload.file.done', { path: item.path, index: index + 1, total: uploadItems.length });
+ }
+ await loadExplorer(win, targetPath);
+}
+
+async function uploadFileChunked(targetPath, file, relativePath) {
+ const chunkSize = 256 * 1024;
+ const totalChunks = Math.max(1, Math.ceil(file.size / chunkSize));
+ let finalPayload = null;
+ for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) {
+ const start = chunkIndex * chunkSize;
+ const chunk = file.slice(start, Math.min(file.size, start + chunkSize));
+ const data = arrayBufferToBase64(await chunk.arrayBuffer());
+ const body = new URLSearchParams();
+ body.set('uploadMode', 'chunk');
+ body.set('relativePath', relativePath);
+ body.set('chunkIndex', String(chunkIndex));
+ body.set('totalChunks', String(totalChunks));
+ body.set('data', data);
+ appendCsrf(body);
+ const response = await fetch(apiUrl(`upload.php?path=${encodeURIComponent(targetPath)}`), {
method: 'POST',
- headers: { 'Content-Type': 'application/octet-stream' },
- body: item.file
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
+ body
});
const text = await response.text();
- recordDebug('upload.file.response', {
- path: item.path,
+ recordDebug('upload.chunk.response', {
+ path: relativePath,
+ chunk: chunkIndex + 1,
+ totalChunks,
status: response.status,
ok: response.ok,
contentType: response.headers.get('content-type'),
@@ -931,18 +943,24 @@ async function uploadFileList(win, files, targetPath) {
if (!response.ok) {
throw new Error(text || `${response.status} ${response.statusText}`);
}
- let payload;
try {
- payload = JSON.parse(text);
+ finalPayload = JSON.parse(text);
} catch (error) {
throw new Error(`Upload lieferte keine JSON-Antwort: ${text.slice(0, 200) || response.headers.get('content-type') || 'leer'}`);
}
- if (!payload.uploaded?.length) {
- throw new Error('Upload wurde vom Server nicht bestätigt.');
- }
- recordDebug('upload.file.done', { path: item.path, index: index + 1, total: uploadItems.length });
}
- await loadExplorer(win, targetPath);
+ if (!finalPayload?.uploaded?.length) {
+ throw new Error('Upload wurde vom Server nicht bestätigt.');
+ }
+}
+
+function arrayBufferToBase64(buffer) {
+ const bytes = new Uint8Array(buffer);
+ let binary = '';
+ for (let index = 0; index < bytes.length; index += 0x8000) {
+ binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000));
+ }
+ return btoa(binary);
}
function normalizeUploadItems(files) {
diff --git a/u-navigator.plg b/u-navigator.plg
index e48cdde..bfc5118 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;
-3c9e5a641720371ec7cee8bd70a32d8e
+45fd0a1c4f8033d9daa56268f48742d4