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