Show upload progress in transfers
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.r049"
|
VERSION="2026.06.24.r001"
|
||||||
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.
+124
-6
@@ -19,6 +19,8 @@ const state = {
|
|||||||
debug: [],
|
debug: [],
|
||||||
nextDebugId: 1,
|
nextDebugId: 1,
|
||||||
jobs: new Map(),
|
jobs: new Map(),
|
||||||
|
uploadTransfers: new Map(),
|
||||||
|
nextUploadTransferId: 1,
|
||||||
jobRefresh: new Map(),
|
jobRefresh: new Map(),
|
||||||
sizeJobs: new Map(),
|
sizeJobs: new Map(),
|
||||||
sizeResults: new Map(),
|
sizeResults: new Map(),
|
||||||
@@ -634,13 +636,18 @@ function renderQueue(body) {
|
|||||||
|
|
||||||
function queueMarkup() {
|
function queueMarkup() {
|
||||||
const jobs = [...state.jobs.values()].reverse();
|
const jobs = [...state.jobs.values()].reverse();
|
||||||
const jobMarkup = jobs.length ? jobs.map((job) => `
|
const uploads = [...state.uploadTransfers.values()].reverse();
|
||||||
|
const transferItems = [
|
||||||
|
...uploads.map(uploadTransferMarkup),
|
||||||
|
...jobs.map((job) => `
|
||||||
<article class="job">
|
<article class="job">
|
||||||
<strong>${escapeHtml(job.type)} #${escapeHtml(job.id)}</strong>
|
<strong>${escapeHtml(job.type)} #${escapeHtml(job.id)}</strong>
|
||||||
<div class="muted">${escapeHtml(job.status)} · ${escapeHtml(job.message || '')}</div>
|
<div class="muted">${escapeHtml(job.status)} · ${escapeHtml(job.message || '')}</div>
|
||||||
<div class="progress"><div style="width:${Number(job.progress) || 0}%"></div></div>
|
<div class="progress"><div style="width:${Number(job.progress) || 0}%"></div></div>
|
||||||
</article>
|
</article>
|
||||||
`).join('') : '<p class="muted">Keine Transfers.</p>';
|
`)
|
||||||
|
];
|
||||||
|
const jobMarkup = transferItems.length ? transferItems.join('') : '<p class="muted">Keine Transfers.</p>';
|
||||||
const debugSection = state.config?.debug ? debugMarkup() : '';
|
const debugSection = state.config?.debug ? debugMarkup() : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -649,6 +656,22 @@ function queueMarkup() {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uploadTransferMarkup(upload) {
|
||||||
|
const progress = Math.max(0, Math.min(100, Number(upload.progress) || 0));
|
||||||
|
const fileText = `${upload.completedFiles + upload.skippedFiles}/${upload.totalFiles} Dateien`;
|
||||||
|
const byteText = `${formatBytes(upload.uploadedBytes)} / ${formatBytes(upload.totalBytes)}`;
|
||||||
|
const detail = upload.currentFile ? ` · ${upload.currentFile}` : '';
|
||||||
|
const skipped = upload.skippedFiles ? ` · ${upload.skippedFiles} übersprungen` : '';
|
||||||
|
const overwritten = upload.overwrittenFiles ? ` · ${upload.overwrittenFiles} überschrieben` : '';
|
||||||
|
return `
|
||||||
|
<article class="job upload-job">
|
||||||
|
<strong>Upload #${escapeHtml(upload.id)}</strong>
|
||||||
|
<div class="muted">${escapeHtml(upload.status)} · ${escapeHtml(fileText)} · ${escapeHtml(byteText)}${escapeHtml(detail)}${escapeHtml(skipped)}${escapeHtml(overwritten)}</div>
|
||||||
|
<div class="progress"><div style="width:${progress}%"></div></div>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function debugMarkup() {
|
function debugMarkup() {
|
||||||
const debugMarkup = state.debug.length ? state.debug.slice(-12).reverse().map((item) => `
|
const debugMarkup = state.debug.length ? state.debug.slice(-12).reverse().map((item) => `
|
||||||
<article class="debug-entry">
|
<article class="debug-entry">
|
||||||
@@ -726,7 +749,9 @@ function scheduleTransferAutoHide() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function activeTransferCount() {
|
function activeTransferCount() {
|
||||||
return [...state.jobs.values()].filter((job) => job.type !== 'size' && ['queued', 'running', 'cancel_requested'].includes(job.status)).length;
|
const activeJobs = [...state.jobs.values()].filter((job) => job.type !== 'size' && ['queued', 'running', 'cancel_requested'].includes(job.status)).length;
|
||||||
|
const activeUploads = [...state.uploadTransfers.values()].filter((upload) => ['queued', 'running'].includes(upload.status)).length;
|
||||||
|
return activeJobs + activeUploads;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTransferPanel() {
|
function renderTransferPanel() {
|
||||||
@@ -902,29 +927,68 @@ async function uploadFileList(win, files, targetPath) {
|
|||||||
}
|
}
|
||||||
if (!files.length) return;
|
if (!files.length) return;
|
||||||
const uploadItems = normalizeUploadItems(files);
|
const uploadItems = normalizeUploadItems(files);
|
||||||
|
const transfer = createUploadTransfer(targetPath, uploadItems);
|
||||||
for (const [index, item] of uploadItems.entries()) {
|
for (const [index, item] of uploadItems.entries()) {
|
||||||
|
updateUploadTransfer(transfer, {
|
||||||
|
status: 'running',
|
||||||
|
currentFile: item.path,
|
||||||
|
currentIndex: index + 1
|
||||||
|
});
|
||||||
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 });
|
||||||
try {
|
try {
|
||||||
await uploadFileChunked(targetPath, item.file, item.path, false);
|
await uploadFileChunked(targetPath, item.file, item.path, false, (bytes) => {
|
||||||
|
addUploadProgress(transfer, bytes);
|
||||||
|
});
|
||||||
|
updateUploadTransfer(transfer, {
|
||||||
|
completedFiles: transfer.completedFiles + 1,
|
||||||
|
currentFile: item.path,
|
||||||
|
progress: uploadProgressPercent(transfer)
|
||||||
|
});
|
||||||
recordDebug('upload.file.done', { path: item.path, index: index + 1, total: uploadItems.length, overwrite: false });
|
recordDebug('upload.file.done', { path: item.path, index: index + 1, total: uploadItems.length, overwrite: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.status !== 409) {
|
if (error.status !== 409) {
|
||||||
|
updateUploadTransfer(transfer, {
|
||||||
|
status: 'failed',
|
||||||
|
message: error.message || 'Upload fehlgeschlagen',
|
||||||
|
currentFile: item.path,
|
||||||
|
progress: uploadProgressPercent(transfer)
|
||||||
|
}, true);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const overwrite = window.confirm(`"${item.path}" existiert bereits.\n\nVorhandene Datei überschreiben?`);
|
const overwrite = window.confirm(`"${item.path}" existiert bereits.\n\nVorhandene Datei überschreiben?`);
|
||||||
recordDebug('upload.overwrite.prompt', { path: item.path, overwrite });
|
recordDebug('upload.overwrite.prompt', { path: item.path, overwrite });
|
||||||
if (!overwrite) {
|
if (!overwrite) {
|
||||||
|
updateUploadTransfer(transfer, {
|
||||||
|
skippedFiles: transfer.skippedFiles + 1,
|
||||||
|
currentFile: item.path,
|
||||||
|
progress: uploadProgressPercent(transfer)
|
||||||
|
});
|
||||||
recordDebug('upload.file.skipped', { path: item.path, index: index + 1, total: uploadItems.length });
|
recordDebug('upload.file.skipped', { path: item.path, index: index + 1, total: uploadItems.length });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await uploadFileChunked(targetPath, item.file, item.path, true);
|
await uploadFileChunked(targetPath, item.file, item.path, true, (bytes) => {
|
||||||
|
addUploadProgress(transfer, bytes);
|
||||||
|
});
|
||||||
|
updateUploadTransfer(transfer, {
|
||||||
|
completedFiles: transfer.completedFiles + 1,
|
||||||
|
overwrittenFiles: transfer.overwrittenFiles + 1,
|
||||||
|
currentFile: item.path,
|
||||||
|
progress: uploadProgressPercent(transfer)
|
||||||
|
});
|
||||||
recordDebug('upload.file.done', { path: item.path, index: index + 1, total: uploadItems.length, overwrite: true });
|
recordDebug('upload.file.done', { path: item.path, index: index + 1, total: uploadItems.length, overwrite: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateUploadTransfer(transfer, {
|
||||||
|
status: 'completed',
|
||||||
|
message: 'Abgeschlossen',
|
||||||
|
currentFile: '',
|
||||||
|
progress: 100
|
||||||
|
}, true);
|
||||||
|
scheduleTransferAutoHide();
|
||||||
await loadExplorer(win, targetPath);
|
await loadExplorer(win, targetPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadFileChunked(targetPath, file, relativePath, overwrite = false) {
|
async function uploadFileChunked(targetPath, file, relativePath, overwrite = false, onChunkUploaded = null) {
|
||||||
const chunkSize = 256 * 1024;
|
const chunkSize = 256 * 1024;
|
||||||
const totalChunks = Math.max(1, Math.ceil(file.size / chunkSize));
|
const totalChunks = Math.max(1, Math.ceil(file.size / chunkSize));
|
||||||
let finalPayload = null;
|
let finalPayload = null;
|
||||||
@@ -963,6 +1027,7 @@ async function uploadFileChunked(targetPath, file, relativePath, overwrite = fal
|
|||||||
error.path = relativePath;
|
error.path = relativePath;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
onChunkUploaded?.(chunk.size);
|
||||||
try {
|
try {
|
||||||
finalPayload = JSON.parse(text);
|
finalPayload = JSON.parse(text);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -974,6 +1039,59 @@ async function uploadFileChunked(targetPath, file, relativePath, overwrite = fal
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createUploadTransfer(targetPath, uploadItems) {
|
||||||
|
const transfer = {
|
||||||
|
id: `upload-${state.nextUploadTransferId++}`,
|
||||||
|
status: 'queued',
|
||||||
|
message: 'Wartet',
|
||||||
|
targetPath,
|
||||||
|
totalFiles: uploadItems.length,
|
||||||
|
completedFiles: 0,
|
||||||
|
skippedFiles: 0,
|
||||||
|
overwrittenFiles: 0,
|
||||||
|
totalBytes: uploadItems.reduce((sum, item) => sum + Number(item.file?.size || 0), 0),
|
||||||
|
uploadedBytes: 0,
|
||||||
|
currentFile: '',
|
||||||
|
currentIndex: 0,
|
||||||
|
progress: 0,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
lastRenderAt: 0
|
||||||
|
};
|
||||||
|
state.uploadTransfers.set(transfer.id, transfer);
|
||||||
|
showTransferPanel();
|
||||||
|
updateTransferPanel();
|
||||||
|
return transfer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUploadTransfer(transfer, patch, force = false) {
|
||||||
|
Object.assign(transfer, patch, {
|
||||||
|
message: patch.message ?? `${transfer.currentIndex || 0}/${transfer.totalFiles}`,
|
||||||
|
updatedAt: Date.now()
|
||||||
|
});
|
||||||
|
const now = Date.now();
|
||||||
|
if (force || now - transfer.lastRenderAt > 180) {
|
||||||
|
transfer.lastRenderAt = now;
|
||||||
|
updateTransferPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUploadProgress(transfer, bytes) {
|
||||||
|
transfer.uploadedBytes += Number(bytes || 0);
|
||||||
|
updateUploadTransfer(transfer, {
|
||||||
|
progress: uploadProgressPercent(transfer)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadProgressPercent(transfer) {
|
||||||
|
if (transfer.totalBytes > 0) {
|
||||||
|
return (transfer.uploadedBytes / transfer.totalBytes) * 100;
|
||||||
|
}
|
||||||
|
if (transfer.totalFiles > 0) {
|
||||||
|
return ((transfer.completedFiles + transfer.skippedFiles) / transfer.totalFiles) * 100;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
function arrayBufferToBase64(buffer) {
|
function arrayBufferToBase64(buffer) {
|
||||||
const bytes = new Uint8Array(buffer);
|
const bytes = new Uint8Array(buffer);
|
||||||
let binary = '';
|
let binary = '';
|
||||||
|
|||||||
@@ -809,6 +809,14 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.u-nav .job .muted {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-nav .upload-job {
|
||||||
|
border-color: color-mix(in srgb, var(--accent-2) 45%, var(--line));
|
||||||
|
}
|
||||||
|
|
||||||
.u-nav .progress {
|
.u-nav .progress {
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
|||||||
+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.r049">
|
<!ENTITY version "2026.06.24.r001">
|
||||||
<!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>3bf3f4752ed9cd8b86fd27f9e17fbe14</MD5>
|
<MD5>5d36280f152114104067f4d3e3a97234</MD5>
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Run="/bin/bash">
|
<FILE Run="/bin/bash">
|
||||||
|
|||||||
Reference in New Issue
Block a user