Upload files via form chunks
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.r047"
|
VERSION="2026.06.23.r048"
|
||||||
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.
@@ -7,6 +7,44 @@ if (!is_dir($target['path'])) {
|
|||||||
unav_error(400, 'Upload target must be a directory');
|
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'])) {
|
if (isset($_GET['relativePath']) || isset($_SERVER['HTTP_X_UNAV_RELATIVE_PATH'])) {
|
||||||
$relativeName = (string)($_GET['relativePath'] ?? $_SERVER['HTTP_X_UNAV_RELATIVE_PATH']);
|
$relativeName = (string)($_GET['relativePath'] ?? $_SERVER['HTTP_X_UNAV_RELATIVE_PATH']);
|
||||||
$destination = unav_prepare_upload_destination($target['path'], $relativeName);
|
$destination = unav_prepare_upload_destination($target['path'], $relativeName);
|
||||||
|
|||||||
+41
-23
@@ -904,25 +904,37 @@ async function uploadFileList(win, files, targetPath) {
|
|||||||
const uploadItems = normalizeUploadItems(files);
|
const uploadItems = normalizeUploadItems(files);
|
||||||
for (const [index, item] of uploadItems.entries()) {
|
for (const [index, item] of uploadItems.entries()) {
|
||||||
recordDebug('upload.file.request', { path: item.path, size: item.file.size, index: index + 1, total: uploadItems.length });
|
recordDebug('upload.file.request', { path: item.path, size: item.file.size, index: index + 1, total: uploadItems.length });
|
||||||
const params = new URLSearchParams({
|
await uploadFileChunked(targetPath, item.file, item.path);
|
||||||
path: targetPath,
|
recordDebug('upload.file.done', { path: item.path, index: index + 1, total: uploadItems.length });
|
||||||
relativePath: item.path
|
}
|
||||||
});
|
await loadExplorer(win, targetPath);
|
||||||
const token = getCsrfToken();
|
}
|
||||||
if (token) {
|
|
||||||
params.set('csrf_token', token);
|
async function uploadFileChunked(targetPath, file, relativePath) {
|
||||||
recordDebug('csrf.attached', { length: token.length });
|
const chunkSize = 256 * 1024;
|
||||||
} else {
|
const totalChunks = Math.max(1, Math.ceil(file.size / chunkSize));
|
||||||
recordDebug('csrf.missing', {});
|
let finalPayload = null;
|
||||||
}
|
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) {
|
||||||
const response = await fetch(apiUrl(`upload.php?${params.toString()}`), {
|
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/octet-stream' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
body: item.file
|
body
|
||||||
});
|
});
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
recordDebug('upload.file.response', {
|
recordDebug('upload.chunk.response', {
|
||||||
path: item.path,
|
path: relativePath,
|
||||||
|
chunk: chunkIndex + 1,
|
||||||
|
totalChunks,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
ok: response.ok,
|
ok: response.ok,
|
||||||
contentType: response.headers.get('content-type'),
|
contentType: response.headers.get('content-type'),
|
||||||
@@ -931,18 +943,24 @@ async function uploadFileList(win, files, targetPath) {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(text || `${response.status} ${response.statusText}`);
|
throw new Error(text || `${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
let payload;
|
|
||||||
try {
|
try {
|
||||||
payload = JSON.parse(text);
|
finalPayload = JSON.parse(text);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Upload lieferte keine JSON-Antwort: ${text.slice(0, 200) || response.headers.get('content-type') || 'leer'}`);
|
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) {
|
function normalizeUploadItems(files) {
|
||||||
|
|||||||
+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.r047">
|
<!ENTITY version "2026.06.23.r048">
|
||||||
<!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>3c9e5a641720371ec7cee8bd70a32d8e</MD5>
|
<MD5>45fd0a1c4f8033d9daa56268f48742d4</MD5>
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Run="/bin/bash">
|
<FILE Run="/bin/bash">
|
||||||
|
|||||||
Reference in New Issue
Block a user