🐛 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:
parent
d601bd7986
commit
6d3b2d3a86
@ -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');
|
||||||
|
|||||||
@ -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`;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
4
main.js
4
main.js
@ -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`);
|
||||||
|
|||||||
@ -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,35 +1347,43 @@ async function startUpload() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
queueJobs.forEach(j => {
|
try {
|
||||||
if (j.status === 'preview') j.status = 'queued';
|
queueJobs.forEach(j => {
|
||||||
});
|
if (j.status === 'preview') j.status = 'queued';
|
||||||
updateQueueActionButtons();
|
});
|
||||||
renderQueueTable();
|
updateQueueActionButtons();
|
||||||
updateStatusBar();
|
renderQueueTable();
|
||||||
|
updateStatusBar();
|
||||||
|
|
||||||
const uploadPayload = {
|
const uploadPayload = {
|
||||||
hosters,
|
hosters,
|
||||||
jobs: jobsToStart.map((job) => ({
|
jobs: jobsToStart.map((job) => ({
|
||||||
id: job.id,
|
id: job.id,
|
||||||
file: job.file,
|
file: job.file,
|
||||||
fileName: job.fileName,
|
fileName: job.fileName,
|
||||||
hoster: job.hoster
|
hoster: job.hoster
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
const result = await window.api.startUpload(uploadPayload);
|
const result = await window.api.startUpload(uploadPayload);
|
||||||
persistQueueStateSoon();
|
persistQueueStateSoon();
|
||||||
|
|
||||||
if (result && result.error) {
|
if (result && result.error) {
|
||||||
alert(result.error);
|
alert(result.error);
|
||||||
|
uploading = false;
|
||||||
|
updateQueueActionButtons();
|
||||||
|
updateStatusBar();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
uploading = false;
|
uploading = false;
|
||||||
updateQueueActionButtons();
|
updateQueueActionButtons();
|
||||||
updateStatusBar();
|
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,30 +1417,37 @@ async function startSelectedUpload() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jobsToStart.forEach(j => {
|
try {
|
||||||
if (j.status === 'preview') j.status = 'queued';
|
jobsToStart.forEach(j => {
|
||||||
});
|
if (j.status === 'preview') j.status = 'queued';
|
||||||
updateQueueActionButtons();
|
});
|
||||||
renderQueueTable();
|
updateQueueActionButtons();
|
||||||
updateStatusBar();
|
renderQueueTable();
|
||||||
|
updateStatusBar();
|
||||||
|
|
||||||
const uploadPayload = {
|
const uploadPayload = {
|
||||||
hosters,
|
hosters,
|
||||||
jobs: jobsToStart.map((job) => ({
|
jobs: jobsToStart.map((job) => ({
|
||||||
id: job.id,
|
id: job.id,
|
||||||
file: job.file,
|
file: job.file,
|
||||||
fileName: job.fileName,
|
fileName: job.fileName,
|
||||||
hoster: job.hoster
|
hoster: job.hoster
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
const result = await window.api.startUpload(uploadPayload);
|
const result = await window.api.startUpload(uploadPayload);
|
||||||
persistQueueStateSoon();
|
persistQueueStateSoon();
|
||||||
|
|
||||||
if (result && result.error) {
|
if (result && result.error) {
|
||||||
alert(result.error);
|
alert(result.error);
|
||||||
|
uploading = false;
|
||||||
|
updateQueueActionButtons();
|
||||||
|
updateStatusBar();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
uploading = false;
|
uploading = false;
|
||||||
updateQueueActionButtons();
|
updateQueueActionButtons();
|
||||||
updateStatusBar();
|
updateStatusBar();
|
||||||
|
alert(`Upload-Start fehlgeschlagen: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user