Compare commits

...

6 Commits

Author SHA1 Message Date
Administrator
169817f707 release: v3.3.50 2026-06-08 14:20:16 +02:00
Administrator
1418c2bc17 feat(backup): plain JSON export/import + clearer error when decrypt fails 2026-06-08 14:19:47 +02:00
Administrator
8d33141294 release: v3.3.49 2026-06-08 03:04:25 +02:00
Administrator
35341b522a fix(accounts): allow health check during active uploads + toast when already running 2026-06-08 03:04:00 +02:00
Administrator
f9aa7f4168 release: v3.3.48 2026-06-08 01:30:19 +02:00
Administrator
d9199f8aaf fix(perf): chunked startBatch + async rotLog — kill remaining 30s freeze on 5k+ jobs 2026-06-08 01:29:31 +02:00
4 changed files with 72 additions and 41 deletions

View File

@ -310,18 +310,30 @@ class UploadManager extends EventEmitter {
this._batchResults = results; this._batchResults = results;
this._additionalPromises = []; // Track jobs added mid-batch via addJobs() this._additionalPromises = []; // Track jobs added mid-batch via addJobs()
for (const task of tasks) { const DEDUP_CHUNK = 200;
const fileName = path.basename(task.file); for (let i = 0; i < tasks.length; i += DEDUP_CHUNK) {
const end = Math.min(i + DEDUP_CHUNK, tasks.length);
for (let j = i; j < end; j++) {
const task = tasks[j];
if (!results.has(task.file)) { if (!results.has(task.file)) {
const fileName = path.basename(task.file);
let size = 0; let size = 0;
try { size = fs.statSync(task.file).size; } catch {} try { size = fs.statSync(task.file).size; } catch {}
results.set(task.file, { name: fileName, size, results: [] }); results.set(task.file, { name: fileName, size, results: [] });
} }
} }
if (end < tasks.length) await new Promise(setImmediate);
}
this._startStatsTimer(); this._startStatsTimer();
const promises = tasks.map((task) => this._runJob(task, results, signal)); const SPAWN_CHUNK = 100;
const promises = [];
for (let i = 0; i < tasks.length; i += SPAWN_CHUNK) {
const end = Math.min(i + SPAWN_CHUNK, tasks.length);
for (let j = i; j < end; j++) promises.push(this._runJob(tasks[j], results, signal));
if (end < tasks.length) await new Promise(setImmediate);
}
await Promise.allSettled(promises); await Promise.allSettled(promises);
// Wait for any jobs added mid-batch via addJobs() // Wait for any jobs added mid-batch via addJobs()
while (this._additionalPromises.length > 0) { while (this._additionalPromises.length > 0) {

47
main.js
View File

@ -216,23 +216,10 @@ function rotLog(msg, ts) {
try { try {
const iso = new Date(ts || Date.now()).toISOString(); const iso = new Date(ts || Date.now()).toISOString();
const line = `[${iso}] ${msg}\n`; const line = `[${iso}] ${msg}\n`;
// Write synchronously. Rotation events are rare (a handful per batch) so _rotLogBuffer.push(line);
// the batching optimization from debugLog doesn't buy us anything, and if (!_rotLogFlushTimer) {
// syncing guarantees the user can refresh the file and see fresh entries _rotLogFlushTimer = setTimeout(() => { _rotLogFlushTimer = null; _flushRotLog(); }, 500);
// without waiting on a flush timer.
const candidates = [
getRotLogPath(),
path.join(app.getPath('desktop') || app.getPath('userData'), 'account-rotation.log'),
path.join(app.getPath('userData'), 'account-rotation.log')
];
for (const target of candidates) {
try {
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.appendFileSync(target, line, 'utf-8');
break;
} catch {}
} }
// Mirror into the main debug log for single-file-grep convenience.
_debugLogBuffer.push(`[${iso}] [ROT] ${msg}\n`); _debugLogBuffer.push(`[${iso}] [ROT] ${msg}\n`);
if (!_debugLogFlushTimer) { if (!_debugLogFlushTimer) {
_debugLogFlushTimer = setTimeout(() => { _debugLogFlushTimer = null; _flushDebugLog(); }, 500); _debugLogFlushTimer = setTimeout(() => { _debugLogFlushTimer = null; _flushDebugLog(); }, 500);
@ -1763,12 +1750,19 @@ ipcMain.handle('export-backup', async () => {
const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, { const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, {
title: 'Backup exportieren', title: 'Backup exportieren',
defaultPath: `multi-hoster-backup-${new Date().toISOString().slice(0, 10)}.mhu`, defaultPath: `multi-hoster-backup-${new Date().toISOString().slice(0, 10)}.mhu`,
filters: [{ name: 'Multi-Hoster Backup', extensions: ['mhu'] }] filters: [
{ name: 'Multi-Hoster Backup (verschlüsselt)', extensions: ['mhu'] },
{ name: 'Multi-Hoster Backup (Klartext JSON)', extensions: ['json'] }
]
}); });
if (canceled || !filePath) return { ok: false, canceled: true }; if (canceled || !filePath) return { ok: false, canceled: true };
const config = configStore.load(); const config = configStore.load();
if (filePath.toLowerCase().endsWith('.json')) {
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf-8');
} else {
const encrypted = backupCrypto.encrypt(config); const encrypted = backupCrypto.encrypt(config);
fs.writeFileSync(filePath, encrypted); fs.writeFileSync(filePath, encrypted);
}
return { ok: true, path: filePath }; return { ok: true, path: filePath };
}); });
@ -1780,7 +1774,11 @@ ipcMain.handle('import-backup', async (_event, legacyPassword) => {
} else { } else {
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, { const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
title: 'Backup importieren', title: 'Backup importieren',
filters: [{ name: 'Multi-Hoster Backup', extensions: ['mhu'] }], filters: [
{ name: 'Multi-Hoster Backup', extensions: ['mhu', 'json'] },
{ name: 'Verschlüsselt (.mhu)', extensions: ['mhu'] },
{ name: 'Klartext (.json)', extensions: ['json'] }
],
properties: ['openFile'] properties: ['openFile']
}); });
if (canceled || !filePaths.length) return { ok: false, canceled: true }; if (canceled || !filePaths.length) return { ok: false, canceled: true };
@ -1789,15 +1787,26 @@ ipcMain.handle('import-backup', async (_event, legacyPassword) => {
_lastImportPath = sourcePath; _lastImportPath = sourcePath;
} }
let imported; let imported;
const looksLikeJson = buffer.length >= 1 && (buffer[0] === 0x7B || buffer[0] === 0x20 || buffer[0] === 0x0A || buffer[0] === 0x0D || buffer[0] === 0x09 || buffer[0] === 0xEF);
if (looksLikeJson) {
try {
const text = buffer.toString('utf-8').replace(/^\uFEFF/, '');
imported = JSON.parse(text);
} catch (err) {
_lastImportPath = null;
return { ok: false, error: 'Klartext-Backup ist kein gültiges JSON: ' + (err.message || err) };
}
} else {
try { try {
imported = backupCrypto.decrypt(buffer, legacyPassword); imported = backupCrypto.decrypt(buffer, legacyPassword);
} catch (err) { } catch (err) {
if (err && err.needsPassword) { if (err && err.needsPassword) {
return { ok: false, needsPassword: true }; return { ok: false, needsPassword: true, hint: 'Falls dieses Backup mit der aktuellen Version erzeugt wurde, ist die Datei vermutlich beim Transfer beschädigt worden (z. B. FTP-Text-Modus). Versuch es mit einem Klartext-JSON-Export.' };
} }
_lastImportPath = null; _lastImportPath = null;
throw err; throw err;
} }
}
_lastImportPath = null; _lastImportPath = null;
// Validate imported data has required structure // Validate imported data has required structure
if (!imported || typeof imported !== 'object' || !imported.hosters || !imported.hosterSettings || !imported.globalSettings) { if (!imported || typeof imported !== 'object' || !imported.hosters || !imported.hosterSettings || !imported.globalSettings) {

View File

@ -1,6 +1,6 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "3.3.47", "version": "3.3.50",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View File

@ -1443,7 +1443,7 @@ async function doBackupExport() {
} }
} }
function askLegacyBackupPassword() { function askLegacyBackupPassword(hint) {
return new Promise((resolve) => { return new Promise((resolve) => {
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.className = 'modal-overlay'; overlay.className = 'modal-overlay';
@ -1456,7 +1456,7 @@ function askLegacyBackupPassword() {
const header = document.createElement('div'); const header = document.createElement('div');
header.className = 'modal-header'; header.className = 'modal-header';
const h3 = document.createElement('h3'); const h3 = document.createElement('h3');
h3.textContent = 'Passwort erforderlich'; h3.textContent = 'Backup nicht entschlüsselbar';
header.appendChild(h3); header.appendChild(h3);
const body = document.createElement('div'); const body = document.createElement('div');
@ -1464,7 +1464,15 @@ function askLegacyBackupPassword() {
const p = document.createElement('p'); const p = document.createElement('p');
p.style.margin = '0 0 10px'; p.style.margin = '0 0 10px';
p.style.fontSize = '13px'; p.style.fontSize = '13px';
p.textContent = 'Dieses Backup wurde mit einem Passwort verschlüsselt.'; p.textContent = 'Wenn das Backup mit der alten Passwort-Option (vor v3.0) erstellt wurde, hier eingeben.';
if (hint) {
const p2 = document.createElement('p');
p2.style.margin = '0 0 10px';
p2.style.fontSize = '12px';
p2.style.color = 'var(--text-dim)';
p2.textContent = hint;
body.appendChild(p2);
}
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'password'; input.type = 'password';
input.className = 'key-input'; input.className = 'key-input';
@ -1508,7 +1516,7 @@ async function doBackupImport(legacyPassword) {
const result = await window.api.importBackup(pw); const result = await window.api.importBackup(pw);
if (!result || result.canceled) return; if (!result || result.canceled) return;
if (result.needsPassword) { if (result.needsPassword) {
const entered = await askLegacyBackupPassword(); const entered = await askLegacyBackupPassword(result.hint);
if (entered) doBackupImport(entered); if (entered) doBackupImport(entered);
return; return;
} }
@ -2522,8 +2530,10 @@ async function executeHealthCheck(hosters, _mode) {
} }
async function runHealthCheck(mode = 'manual', requestedHosters = null) { async function runHealthCheck(mode = 'manual', requestedHosters = null) {
if (healthCheckRunning || (uploading && mode === 'manual')) return []; if (healthCheckRunning) {
// Build check list: all enabled accounts with creds if (mode === 'manual') showCopyToast('Account-Check läuft bereits.');
return [];
}
let hosters; let hosters;
if (Array.isArray(requestedHosters) && requestedHosters.length > 0) { if (Array.isArray(requestedHosters) && requestedHosters.length > 0) {
hosters = requestedHosters; hosters = requestedHosters;