Show upload progress in transfers

This commit is contained in:
Mikei386
2026-06-24 13:04:42 +02:00
parent aa58014cd7
commit 6df1c7ea97
6 changed files with 135 additions and 9 deletions
+124 -6
View File
@@ -19,6 +19,8 @@ const state = {
debug: [],
nextDebugId: 1,
jobs: new Map(),
uploadTransfers: new Map(),
nextUploadTransferId: 1,
jobRefresh: new Map(),
sizeJobs: new Map(),
sizeResults: new Map(),
@@ -634,13 +636,18 @@ function renderQueue(body) {
function queueMarkup() {
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">
<strong>${escapeHtml(job.type)} #${escapeHtml(job.id)}</strong>
<div class="muted">${escapeHtml(job.status)} · ${escapeHtml(job.message || '')}</div>
<div class="progress"><div style="width:${Number(job.progress) || 0}%"></div></div>
</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() : '';
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() {
const debugMarkup = state.debug.length ? state.debug.slice(-12).reverse().map((item) => `
<article class="debug-entry">
@@ -726,7 +749,9 @@ function scheduleTransferAutoHide() {
}
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() {
@@ -902,29 +927,68 @@ async function uploadFileList(win, files, targetPath) {
}
if (!files.length) return;
const uploadItems = normalizeUploadItems(files);
const transfer = createUploadTransfer(targetPath, uploadItems);
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 });
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 });
} catch (error) {
if (error.status !== 409) {
updateUploadTransfer(transfer, {
status: 'failed',
message: error.message || 'Upload fehlgeschlagen',
currentFile: item.path,
progress: uploadProgressPercent(transfer)
}, true);
throw error;
}
const overwrite = window.confirm(`"${item.path}" existiert bereits.\n\nVorhandene Datei überschreiben?`);
recordDebug('upload.overwrite.prompt', { path: item.path, 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 });
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 });
}
}
updateUploadTransfer(transfer, {
status: 'completed',
message: 'Abgeschlossen',
currentFile: '',
progress: 100
}, true);
scheduleTransferAutoHide();
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 totalChunks = Math.max(1, Math.ceil(file.size / chunkSize));
let finalPayload = null;
@@ -963,6 +1027,7 @@ async function uploadFileChunked(targetPath, file, relativePath, overwrite = fal
error.path = relativePath;
throw error;
}
onChunkUploaded?.(chunk.size);
try {
finalPayload = JSON.parse(text);
} 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) {
const bytes = new Uint8Array(buffer);
let binary = '';