🐛 fix: upload button stuck, abort handling, filename escaping

- 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) <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-03-21 11:21:09 +01:00
parent d601bd7986
commit 6d3b2d3a86
6 changed files with 65 additions and 38 deletions

View File

@ -210,7 +210,8 @@ class DoodstreamUploader {
let preamble = ''; 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="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="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 epilogue = `\r\n--${boundary}--\r\n`;
const preambleBuf = Buffer.from(preamble, 'utf-8'); const preambleBuf = Buffer.from(preamble, 'utf-8');

View File

@ -232,7 +232,8 @@ function buildMultipart(filePath, formFields) {
preamble += `${value}\r\n`; preamble += `${value}\r\n`;
} }
preamble += `--${boundary}\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`; preamble += `Content-Type: application/octet-stream\r\n\r\n`;
const epilogue = `\r\n--${boundary}--\r\n`; const epilogue = `\r\n--${boundary}--\r\n`;

View File

@ -200,7 +200,8 @@ class VidmolyUploader {
preamble += `${value}\r\n`; preamble += `${value}\r\n`;
} }
preamble += `--${boundary}\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`; preamble += `Content-Type: application/octet-stream\r\n\r\n`;
const epilogue = `\r\n--${boundary}--\r\n`; const epilogue = `\r\n--${boundary}--\r\n`;
@ -216,6 +217,7 @@ class VidmolyUploader {
yield preambleBuf; yield preambleBuf;
const fileStream = fs.createReadStream(filePath, { highWaterMark: CHUNK_SIZE }); const fileStream = fs.createReadStream(filePath, { highWaterMark: CHUNK_SIZE });
for await (const chunk of fileStream) { for await (const chunk of fileStream) {
if (signal && signal.aborted) throw new Error('Aborted');
if (throttle) await throttle.consume(chunk.length, signal); if (throttle) await throttle.consume(chunk.length, signal);
bytesRead += chunk.length; bytesRead += chunk.length;
yield chunk; yield chunk;

View File

@ -228,7 +228,8 @@ class VoeUploader {
preamble += `${sessionId}\r\n`; preamble += `${sessionId}\r\n`;
} }
preamble += `--${boundary}\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`; preamble += `Content-Type: application/octet-stream\r\n\r\n`;
const epilogue = `\r\n--${boundary}--\r\n`; const epilogue = `\r\n--${boundary}--\r\n`;
@ -244,6 +245,7 @@ class VoeUploader {
yield preambleBuf; yield preambleBuf;
const fileStream = fs.createReadStream(filePath, { highWaterMark: CHUNK_SIZE }); const fileStream = fs.createReadStream(filePath, { highWaterMark: CHUNK_SIZE });
for await (const chunk of fileStream) { for await (const chunk of fileStream) {
if (signal && signal.aborted) throw new Error('Aborted');
if (throttle) await throttle.consume(chunk.length, signal); if (throttle) await throttle.consume(chunk.length, signal);
bytesRead += chunk.length; bytesRead += chunk.length;
yield chunk; yield chunk;

View File

@ -802,6 +802,10 @@ ipcMain.handle('import-backup', async (_event, password) => {
if (canceled || !filePaths.length) return { ok: false, canceled: true }; if (canceled || !filePaths.length) return { ok: false, canceled: true };
const buffer = fs.readFileSync(filePaths[0]); const buffer = fs.readFileSync(filePaths[0]);
const imported = backupCrypto.decrypt(buffer, password); 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 // Safety net: timestamped backup so multiple imports don't overwrite each other
const ts = new Date().toISOString().replace(/[:.]/g, '-'); const ts = new Date().toISOString().replace(/[:.]/g, '-');
const preImportPath = configStore.filePath.replace('.json', `.pre-import-${ts}.json`); const preImportPath = configStore.filePath.replace('.json', `.pre-import-${ts}.json`);

View File

@ -1310,7 +1310,9 @@ function getSelectedJobLinks() {
// --- Upload --- // --- Upload ---
async function startUpload() { 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 uploading = true; // set immediately to prevent double-click race
updateQueueActionButtons(); updateQueueActionButtons();
@ -1345,6 +1347,7 @@ async function startUpload() {
} }
} }
try {
queueJobs.forEach(j => { queueJobs.forEach(j => {
if (j.status === 'preview') j.status = 'queued'; if (j.status === 'preview') j.status = 'queued';
}); });
@ -1370,10 +1373,17 @@ async function startUpload() {
updateQueueActionButtons(); updateQueueActionButtons();
updateStatusBar(); updateStatusBar();
} }
} catch (err) {
uploading = false;
updateQueueActionButtons();
updateStatusBar();
alert(`Upload-Start fehlgeschlagen: ${err.message}`);
}
} }
async function startSelectedUpload() { 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 uploading = true; // set immediately to prevent double-click race
updateQueueActionButtons(); updateQueueActionButtons();
@ -1407,6 +1417,7 @@ async function startSelectedUpload() {
} }
} }
try {
jobsToStart.forEach(j => { jobsToStart.forEach(j => {
if (j.status === 'preview') j.status = 'queued'; if (j.status === 'preview') j.status = 'queued';
}); });
@ -1432,6 +1443,12 @@ async function startSelectedUpload() {
updateQueueActionButtons(); updateQueueActionButtons();
updateStatusBar(); updateStatusBar();
} }
} catch (err) {
uploading = false;
updateQueueActionButtons();
updateStatusBar();
alert(`Upload-Start fehlgeschlagen: ${err.message}`);
}
} }
async function cancelUpload() { async function cancelUpload() {