🐛 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 = '';
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');

View File

@ -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`;

View File

@ -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;

View File

@ -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;

View File

@ -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`);

View File

@ -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}`);
}
}