diff --git a/build-plugin.sh b/build-plugin.sh index f7f042b..54ab595 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.06.23.r048" +VERSION="2026.06.23.r049" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.06.23.r048.tgz b/packages/u-navigator-2026.06.23.r048.tgz deleted file mode 100644 index 5eee467..0000000 Binary files a/packages/u-navigator-2026.06.23.r048.tgz and /dev/null differ diff --git a/packages/u-navigator-2026.06.23.r049.tgz b/packages/u-navigator-2026.06.23.r049.tgz new file mode 100644 index 0000000..5fdc6dc Binary files /dev/null and b/packages/u-navigator-2026.06.23.r049.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 a821d2d..8176498 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 @@ -12,10 +12,11 @@ if (($_POST['uploadMode'] ?? '') === 'chunk') { $chunkIndex = filter_var($_POST['chunkIndex'] ?? null, FILTER_VALIDATE_INT); $totalChunks = filter_var($_POST['totalChunks'] ?? null, FILTER_VALIDATE_INT); $encoded = (string)($_POST['data'] ?? ''); + $overwrite = unav_upload_bool($_POST['overwrite'] ?? false); 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); + $destination = unav_prepare_upload_destination($target['path'], $relativeName, $overwrite); $temporary = $destination['path'] . '.u-nav-upload'; if ($chunkIndex === 0) { @unlink($temporary); @@ -30,6 +31,16 @@ if (($_POST['uploadMode'] ?? '') === 'chunk') { unav_error(500, 'Could not write upload chunk'); } $complete = $chunkIndex === $totalChunks - 1; + if ($complete && file_exists($destination['path'])) { + if (!$overwrite) { + @unlink($temporary); + unav_error(409, 'Destination already exists'); + } + if (is_dir($destination['path']) || !@unlink($destination['path'])) { + @unlink($temporary); + unav_error(409, 'Destination cannot be overwritten'); + } + } if ($complete && !rename($temporary, $destination['path'])) { @unlink($temporary); unav_error(500, 'Could not finalize upload'); @@ -47,7 +58,11 @@ if (($_POST['uploadMode'] ?? '') === 'chunk') { 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); + $overwrite = unav_upload_bool($_GET['overwrite'] ?? ($_SERVER['HTTP_X_UNAV_OVERWRITE'] ?? false)); + $destination = unav_prepare_upload_destination($target['path'], $relativeName, $overwrite); + if ($overwrite && file_exists($destination['path']) && !is_dir($destination['path']) && !@unlink($destination['path'])) { + unav_error(409, 'Destination cannot be overwritten'); + } $bytes = file_put_contents($destination['path'], file_get_contents('php://input'), LOCK_EX); if ($bytes === false) { unav_error(500, 'Could not store uploaded file'); @@ -61,6 +76,7 @@ if (isset($_GET['relativePath']) || isset($_SERVER['HTTP_X_UNAV_RELATIVE_PATH']) } $uploaded = []; +$overwrite = unav_upload_bool($_POST['overwrite'] ?? false); $names = unav_upload_array($_FILES['files']['name'] ?? []); $errors = unav_upload_array($_FILES['files']['error'] ?? []); $tmpNames = unav_upload_array($_FILES['files']['tmp_name'] ?? []); @@ -70,7 +86,10 @@ foreach ($names as $index => $name) { unav_error(400, 'Upload failed'); } $relativeName = $relativePaths[$index] ?? $name; - $destination = unav_prepare_upload_destination($target['path'], (string)$relativeName); + $destination = unav_prepare_upload_destination($target['path'], (string)$relativeName, $overwrite); + if ($overwrite && file_exists($destination['path']) && !is_dir($destination['path']) && !@unlink($destination['path'])) { + unav_error(409, 'Destination cannot be overwritten'); + } if (!move_uploaded_file($tmpNames[$index] ?? '', $destination['path'])) { unav_error(500, 'Could not store uploaded file'); } @@ -82,7 +101,7 @@ foreach ($names as $index => $name) { unav_json(['uploaded' => $uploaded], 201); -function unav_prepare_upload_destination(string $targetPath, string $relativeName): array { +function unav_prepare_upload_destination(string $targetPath, string $relativeName, bool $overwrite = false): array { $safeName = unav_upload_safe_relative_path($relativeName); $relativeParent = dirname($safeName); if ($relativeParent !== '.') { @@ -94,7 +113,12 @@ function unav_prepare_upload_destination(string $targetPath, string $relativeNam } $destination = unav_destination_path($targetPath . '/' . $safeName); if (file_exists($destination['path'])) { - unav_error(409, 'Destination already exists'); + if (!$overwrite) { + unav_error(409, 'Destination already exists'); + } + if (is_dir($destination['path'])) { + unav_error(409, 'Destination directory already exists'); + } } if (!is_dir(dirname($destination['path']))) { unav_error(500, 'Could not create upload directory'); @@ -126,3 +150,7 @@ function unav_upload_safe_relative_path(string $name): string { } return implode('/', $parts); } + +function unav_upload_bool($value): bool { + return filter_var($value, FILTER_VALIDATE_BOOLEAN); +} diff --git a/server/public/app.js b/server/public/app.js index 69fcb9b..07a95a5 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -904,13 +904,27 @@ 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 }); - await uploadFileChunked(targetPath, item.file, item.path); - recordDebug('upload.file.done', { path: item.path, index: index + 1, total: uploadItems.length }); + try { + await uploadFileChunked(targetPath, item.file, item.path, false); + recordDebug('upload.file.done', { path: item.path, index: index + 1, total: uploadItems.length, overwrite: false }); + } catch (error) { + if (error.status !== 409) { + throw error; + } + const overwrite = window.confirm(`"${item.path}" existiert bereits.\n\nVorhandene Datei überschreiben?`); + recordDebug('upload.overwrite.prompt', { path: item.path, overwrite }); + if (!overwrite) { + recordDebug('upload.file.skipped', { path: item.path, index: index + 1, total: uploadItems.length }); + continue; + } + await uploadFileChunked(targetPath, item.file, item.path, true); + recordDebug('upload.file.done', { path: item.path, index: index + 1, total: uploadItems.length, overwrite: true }); + } } await loadExplorer(win, targetPath); } -async function uploadFileChunked(targetPath, file, relativePath) { +async function uploadFileChunked(targetPath, file, relativePath, overwrite = false) { const chunkSize = 256 * 1024; const totalChunks = Math.max(1, Math.ceil(file.size / chunkSize)); let finalPayload = null; @@ -924,6 +938,9 @@ async function uploadFileChunked(targetPath, file, relativePath) { body.set('chunkIndex', String(chunkIndex)); body.set('totalChunks', String(totalChunks)); body.set('data', data); + if (overwrite) { + body.set('overwrite', '1'); + } appendCsrf(body); const response = await fetch(apiUrl(`upload.php?path=${encodeURIComponent(targetPath)}`), { method: 'POST', @@ -941,7 +958,10 @@ async function uploadFileChunked(targetPath, file, relativePath) { body: text.slice(0, 1000) }); if (!response.ok) { - throw new Error(text || `${response.status} ${response.statusText}`); + const error = new Error(text || `${response.status} ${response.statusText}`); + error.status = response.status; + error.path = relativePath; + throw error; } try { finalPayload = JSON.parse(text); diff --git a/u-navigator.plg b/u-navigator.plg index bfc5118..7d958bf 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; -45fd0a1c4f8033d9daa56268f48742d4 +3bf3f4752ed9cd8b86fd27f9e17fbe14