From 6d3b2d3a860c5e7d0913d1a566f769a6724447c1 Mon Sep 17 00:00:00 2001 From: Administrator Date: Sat, 21 Mar 2026 11:21:09 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20upload=20button=20stuck,?= =?UTF-8?q?=20abort=20handling,=20filename=20escaping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upload button no longer gets permanently stuck if startUpload() throws after health check (try-catch with uploading=false reset) - Wait for running health check instead of silently blocking upload - Add abort signal check in VOE/Vidmoly upload generators - Escape filenames with quotes/backslashes in multipart form headers (all 4 uploaders: doodstream, voe, vidmoly, byse) - Validate backup import structure before overwriting config Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/doodstream-upload.js | 3 +- lib/hosters.js | 3 +- lib/vidmoly-upload.js | 4 +- lib/voe-upload.js | 4 +- main.js | 4 ++ renderer/app.js | 85 ++++++++++++++++++++++++---------------- 6 files changed, 65 insertions(+), 38 deletions(-) diff --git a/lib/doodstream-upload.js b/lib/doodstream-upload.js index aac1baa..caafab7 100644 --- a/lib/doodstream-upload.js +++ b/lib/doodstream-upload.js @@ -210,7 +210,8 @@ class DoodstreamUploader { let preamble = ''; preamble += `--${boundary}\r\nContent-Disposition: form-data; name="sess_id"\r\n\r\n${this.sessId}\r\n`; preamble += `--${boundary}\r\nContent-Disposition: form-data; name="utype"\r\n\r\nreg\r\n`; - preamble += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`; + const safeFileName = fileName.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + preamble += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${safeFileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`; const epilogue = `\r\n--${boundary}--\r\n`; const preambleBuf = Buffer.from(preamble, 'utf-8'); diff --git a/lib/hosters.js b/lib/hosters.js index 4ddd685..80e2b49 100644 --- a/lib/hosters.js +++ b/lib/hosters.js @@ -232,7 +232,8 @@ function buildMultipart(filePath, formFields) { preamble += `${value}\r\n`; } preamble += `--${boundary}\r\n`; - preamble += `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n`; + const safeFileName = fileName.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + preamble += `Content-Disposition: form-data; name="file"; filename="${safeFileName}"\r\n`; preamble += `Content-Type: application/octet-stream\r\n\r\n`; const epilogue = `\r\n--${boundary}--\r\n`; diff --git a/lib/vidmoly-upload.js b/lib/vidmoly-upload.js index 54952df..2156fc2 100644 --- a/lib/vidmoly-upload.js +++ b/lib/vidmoly-upload.js @@ -200,7 +200,8 @@ class VidmolyUploader { preamble += `${value}\r\n`; } preamble += `--${boundary}\r\n`; - preamble += `Content-Disposition: form-data; name="${fileFieldName || 'file'}"; filename="${fileName}"\r\n`; + const safeFileName = fileName.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + preamble += `Content-Disposition: form-data; name="${fileFieldName || 'file'}"; filename="${safeFileName}"\r\n`; preamble += `Content-Type: application/octet-stream\r\n\r\n`; const epilogue = `\r\n--${boundary}--\r\n`; @@ -216,6 +217,7 @@ class VidmolyUploader { yield preambleBuf; const fileStream = fs.createReadStream(filePath, { highWaterMark: CHUNK_SIZE }); for await (const chunk of fileStream) { + if (signal && signal.aborted) throw new Error('Aborted'); if (throttle) await throttle.consume(chunk.length, signal); bytesRead += chunk.length; yield chunk; diff --git a/lib/voe-upload.js b/lib/voe-upload.js index ec2479a..a76c2ca 100644 --- a/lib/voe-upload.js +++ b/lib/voe-upload.js @@ -228,7 +228,8 @@ class VoeUploader { preamble += `${sessionId}\r\n`; } preamble += `--${boundary}\r\n`; - preamble += `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n`; + const safeFileName = fileName.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + preamble += `Content-Disposition: form-data; name="file"; filename="${safeFileName}"\r\n`; preamble += `Content-Type: application/octet-stream\r\n\r\n`; const epilogue = `\r\n--${boundary}--\r\n`; @@ -244,6 +245,7 @@ class VoeUploader { yield preambleBuf; const fileStream = fs.createReadStream(filePath, { highWaterMark: CHUNK_SIZE }); for await (const chunk of fileStream) { + if (signal && signal.aborted) throw new Error('Aborted'); if (throttle) await throttle.consume(chunk.length, signal); bytesRead += chunk.length; yield chunk; diff --git a/main.js b/main.js index 0611a01..55fe9b3 100644 --- a/main.js +++ b/main.js @@ -802,6 +802,10 @@ ipcMain.handle('import-backup', async (_event, password) => { if (canceled || !filePaths.length) return { ok: false, canceled: true }; const buffer = fs.readFileSync(filePaths[0]); const imported = backupCrypto.decrypt(buffer, password); + // Validate imported data has required structure + if (!imported || typeof imported !== 'object' || !imported.hosters || !imported.hosterSettings || !imported.globalSettings) { + return { ok: false, error: 'Backup-Datei hat ungültige Struktur (hosters, hosterSettings oder globalSettings fehlt).' }; + } // Safety net: timestamped backup so multiple imports don't overwrite each other const ts = new Date().toISOString().replace(/[:.]/g, '-'); const preImportPath = configStore.filePath.replace('.json', `.pre-import-${ts}.json`); diff --git a/renderer/app.js b/renderer/app.js index 99a6e87..bdc1c5f 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -1310,7 +1310,9 @@ function getSelectedJobLinks() { // --- Upload --- async function startUpload() { - if (healthCheckRunning || uploading) return; + if (uploading) return; + // Wait for any running health check to finish (e.g. startup auto-check) + while (healthCheckRunning) await new Promise(r => setTimeout(r, 100)); uploading = true; // set immediately to prevent double-click race updateQueueActionButtons(); @@ -1345,35 +1347,43 @@ async function startUpload() { } } - queueJobs.forEach(j => { - if (j.status === 'preview') j.status = 'queued'; - }); - updateQueueActionButtons(); - renderQueueTable(); - updateStatusBar(); + try { + queueJobs.forEach(j => { + if (j.status === 'preview') j.status = 'queued'; + }); + updateQueueActionButtons(); + renderQueueTable(); + updateStatusBar(); - const uploadPayload = { - hosters, - jobs: jobsToStart.map((job) => ({ - id: job.id, - file: job.file, - fileName: job.fileName, - hoster: job.hoster - })) - }; - const result = await window.api.startUpload(uploadPayload); - persistQueueStateSoon(); + const uploadPayload = { + hosters, + jobs: jobsToStart.map((job) => ({ + id: job.id, + file: job.file, + fileName: job.fileName, + hoster: job.hoster + })) + }; + const result = await window.api.startUpload(uploadPayload); + persistQueueStateSoon(); - if (result && result.error) { - alert(result.error); + if (result && result.error) { + alert(result.error); + uploading = false; + updateQueueActionButtons(); + updateStatusBar(); + } + } catch (err) { uploading = false; updateQueueActionButtons(); updateStatusBar(); + alert(`Upload-Start fehlgeschlagen: ${err.message}`); } } async function startSelectedUpload() { - if (healthCheckRunning || uploading) return; + if (uploading) return; + while (healthCheckRunning) await new Promise(r => setTimeout(r, 100)); uploading = true; // set immediately to prevent double-click race updateQueueActionButtons(); @@ -1407,30 +1417,37 @@ async function startSelectedUpload() { } } - jobsToStart.forEach(j => { - if (j.status === 'preview') j.status = 'queued'; - }); - updateQueueActionButtons(); - renderQueueTable(); - updateStatusBar(); + try { + jobsToStart.forEach(j => { + if (j.status === 'preview') j.status = 'queued'; + }); + updateQueueActionButtons(); + renderQueueTable(); + updateStatusBar(); - const uploadPayload = { - hosters, - jobs: jobsToStart.map((job) => ({ + const uploadPayload = { + hosters, + jobs: jobsToStart.map((job) => ({ id: job.id, file: job.file, fileName: job.fileName, hoster: job.hoster })) }; - const result = await window.api.startUpload(uploadPayload); - persistQueueStateSoon(); + const result = await window.api.startUpload(uploadPayload); + persistQueueStateSoon(); - if (result && result.error) { - alert(result.error); + if (result && result.error) { + alert(result.error); + uploading = false; + updateQueueActionButtons(); + updateStatusBar(); + } + } catch (err) { uploading = false; updateQueueActionButtons(); updateStatusBar(); + alert(`Upload-Start fehlgeschlagen: ${err.message}`); } }