From c8838d34b3ad70dd91ed0ddb789545ad63d455cc Mon Sep 17 00:00:00 2001 From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com> Date: Mon, 7 Sep 2026 02:44:08 +0200 Subject: [PATCH] fix: continue VOE account fallback rotation --- PROJECT_MEMORY.md | 8 ++++-- lib/account-rotation.js | 1 + lib/upload-manager.js | 9 ++++++- tests/account-rotation.test.js | 1 + tests/upload-manager.test.js | 46 ++++++++++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md index 6ebb696..9c79bc7 100644 --- a/PROJECT_MEMORY.md +++ b/PROJECT_MEMORY.md @@ -25,6 +25,7 @@ Multi-Hoster-Upload ist eine Electron-Desktopanwendung für Windows, die große - Backup-Importe wenden Sprache und automatischen Account-Check sofort an. Alle Konfigurationsfelder werden übertragen; Warteschlange, Upload-Wiederherstellungsstatus, letzter Dateiauswahlordner, Automatik-Telemetrie und Pausenzustand bleiben bewusst geräte- beziehungsweise laufzeitgebunden. - Ein nicht vorhandener Ordnerüberwachungspfad bleibt nach dem Import sichtbar gespeichert, die Überwachung wird aber deaktiviert und der Nutzer erhält eine Warnung. Ein nicht vorhandener Log-Ordner wird ebenfalls gemeldet, ohne den konfigurierten Pfad still zu löschen. - Upload-Status-Badges und ihre Textlabels sind nicht markierbar; kopierbare Fehlerdetails, Logs und Eingabefelder behalten ihre Textauswahl. +- VOE-Fehler mit der Meldung `Maximum storage space of the account used up.` gelten als temporärer Accountfehler. Die Retry-Schleife bricht auch nach einem bereits erfolgten Account-Wechsel sofort ab und setzt die Fallback-Kette Account für Account fort, bis ein Upload gelingt oder kein weiterer Account verfügbar ist. - Version `2.1.43` ist als GitHub- und Forgejo-Release veröffentlicht; Backup-API `2.0.4` blieb bei diesem reinen Oberflächen-Release unverändert aktiv. - Der eingebaute Updater liest Releases und Binärdateien von Forgejo; GitHub liefert ergänzend die öffentlichen Release Notes. Ein Release ist deshalb erst vollständig, wenn die vier Assets auch im Forgejo-Release vorhanden sind. - Forgejo bewahrt Leerzeichen in Asset-Namen, GitHub normalisiert sie zu Punkten. Das Forgejo-`latest.yml` und der Release-Plan verwenden Namen wie `Multi-Hoster-Upload Setup 2.1.43.exe`; das GitHub-Manifest muss auf den dort tatsächlich veröffentlichten Punktnamen zeigen. @@ -57,15 +58,18 @@ npm audit --omit=dev ## Offene nächste Schritte - Keine offenen Schritte für Release `v2.1.43`; Rollback-Ziel ist Anwendungsversion `2.1.42`. +- Der nach `v2.1.43` behobene VOE-Account-Rotationsfehler ist vollständig getestet, aber noch nicht als neue Anwendungsversion veröffentlicht. - Bei Bedarf einen Arbeitsweg ohne `&` im absoluten Pfad verwenden oder die npm-Aufrufe weiterhin direkt ausführen. ## Zuletzt verifiziert -Stand: 02.09.2026 +Stand: 07.09.2026 - Lint: erfolgreich, 0 Warnungen und 0 Fehler. -- Haupttests: 805 erfolgreich, 0 fehlgeschlagen. +- Haupttests: 807 erfolgreich, 0 fehlgeschlagen. - Backup-API-Tests: 17 erfolgreich, 0 fehlgeschlagen. +- Der Regressionstest für die VOE-Fallback-Kette bestätigt bei deaktivierter normaler Rotation genau einen Versuch auf jedem vollen Account und anschließend den erfolgreichen Wechsel auf den vierten Account. +- Das Support-Bundle vom 07.09.2026 bestätigt als Ursache der gemeldeten Datei: Wechsel vom Primäraccount auf `Fallback #1`, dort vier unnötige Versuche, anschließend `skip-account-pause` und Abbruch mit `override-same-as-current` statt Weiterschaltung. - Der vollständige opt-in UI-Smoke bestätigte zusätzlich, dass Upload-Status-Badges und deren Labels nicht markierbar sind; die 16 bekannten themenfremden Abweichungen blieben unverändert. - Produktionsabhängigkeiten: `npm audit --omit=dev` meldet 0 Schwachstellen. - Release-Commit `5311cdf` und der annotierte Tag `v2.1.43` wurden auf GitHub und Forgejo mit identischen Commit-Hashes verifiziert. diff --git a/lib/account-rotation.js b/lib/account-rotation.js index 27ac9c8..801b20a 100644 --- a/lib/account-rotation.js +++ b/lib/account-rotation.js @@ -42,6 +42,7 @@ function classifyAccountFailure(error) { const cooldownPatterns = [ /\b429\b|rate[- ]?limit|too many requests/i, /quota|not enough (disk )?(space|storage)|insufficient (disk )?space/i, + /maximum storage space of the account (?:used up|reached|exceeded)/i, /disk (space )?full|storage (exhausted|full|voll|limit)|account (full|voll)/i, /session (expired|abgelaufen)|CSRF[- ]?Token nicht gefunden|not logged in/i, /Keine Session erhalten|Session konnte nicht verifiziert werden/i, diff --git a/lib/upload-manager.js b/lib/upload-manager.js index 0635971..ba5fcf3 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -230,8 +230,8 @@ class UploadManager extends EventEmitter { _shouldSkipRetryOnAccountError(err) { if (!err) return false; if (err.transientNetwork === true) return false; - // Explicit account-level flag from hoster parsers — highest priority. if (err.accountError === true) return true; + if (classifyAccountFailure(err) !== 'none') return true; if (!err.message) return false; const m = String(err.message); const PATTERNS = [ @@ -1109,6 +1109,13 @@ class UploadManager extends EventEmitter { if (this._isFileRejectedError(err)) break; if (this._isHosterTransientError(err)) break; if (this._isTransientNetworkError(err)) break; + if (this._shouldSkipRetryOnAccountError(err)) { + this._rotLog('fast-fail', { + jobId, hoster: task.hoster, fileName, accountId: task.accountId, + attempt, error: err && err.message ? err.message : String(err), rotationRetry: true + }); + break; + } if (attempt >= maxAttempts) break; } } diff --git a/tests/account-rotation.test.js b/tests/account-rotation.test.js index 3c09913..88cd816 100644 --- a/tests/account-rotation.test.js +++ b/tests/account-rotation.test.js @@ -238,6 +238,7 @@ test('account failure classification cools down quota and rate-limit errors with const quota = new Error('not enough disk space on your account'); quota.accountError = true; assert.equal(classifyAccountFailure(quota), 'cooldown'); + assert.equal(classifyAccountFailure(new Error('Maximum storage space of the account used up.')), 'cooldown'); assert.equal(classifyAccountFailure(new Error('HTTP 429 Too Many Requests')), 'cooldown'); const transient = new Error('HTTP 503 Service Unavailable'); transient.transientNetwork = true; diff --git a/tests/upload-manager.test.js b/tests/upload-manager.test.js index 2624483..ebf62d3 100644 --- a/tests/upload-manager.test.js +++ b/tests/upload-manager.test.js @@ -1111,6 +1111,13 @@ describe('UploadManager', () => { 'must not match generic "lehnte Datei ab" as file-rejected'); }); + it('classifies VOE storage exhaustion by message alone', () => { + const mgr = new UploadManager({}); + const err = new Error('Maximum storage space of the account used up.'); + assert.equal(mgr._shouldSkipRetryOnAccountError(err), true); + assert.equal(mgr._isFileRejectedError(err), false); + }); + it('keeps true file rejections as file-rejected', () => { const mgr = new UploadManager({}); const err = new Error('Byse lehnte Datei ab: Duplicate'); @@ -1169,6 +1176,45 @@ describe('UploadManager', () => { assert.deepEqual(successes, [{ hoster: 'byse.sx', accountId: 'fallback' }]); }); + it('advances through every VOE fallback after storage exhaustion', async () => { + const accounts = [ + { id: 'primary', apiKey: 'key-1' }, + { id: 'fallback-1', apiKey: 'key-2' }, + { id: 'fallback-2', apiKey: 'key-3' }, + { id: 'fallback-3', apiKey: 'key-4' } + ]; + const attempts = []; + mockUploadFile.mock.mockImplementation(async (hoster, filePath, apiKey, onProgress) => { + attempts.push(apiKey); + if (apiKey !== 'key-4') throw new Error('Maximum storage space of the account used up.'); + if (onProgress) onProgress(fakeFileSize, fakeFileSize); + return { download_url: 'https://voe.sx/final', embed_url: null, file_code: 'final' }; + }); + const mgr = new UploadManager({ 'voe.sx': { retries: 3, parallelCount: 1, rotateAccounts: false } }); + mgr._sleep = async () => {}; + const paused = []; + const completed = []; + mgr.on('account-paused', event => paused.push(event)); + mgr.on('progress', event => { + if (event.status === 'done') completed.push(event.accountId); + }); + mgr.on('account-failed', ({ hoster, accountId }) => { + const failedIndex = accounts.findIndex(account => account.id === accountId); + const fallback = accounts[failedIndex + 1]; + if (fallback) mgr.switchAccount(hoster, fallback); + }); + + await mgr.startBatch([ + { file: '/test/voe-storage-full.mkv', hoster: 'voe.sx', accountId: 'primary', apiKey: 'key-1' } + ], { + primeOverrides: [['voe.sx', accounts[1]]] + }); + + assert.deepEqual(attempts, ['key-1', 'key-2', 'key-3', 'key-4']); + assert.deepEqual(paused.map(event => event.accountId), ['primary', 'fallback-1', 'fallback-2']); + assert.deepEqual(completed, ['fallback-3']); + }); + it('emits a manual account pause for credential failures', async () => { mockUploadFile.mock.mockImplementation(async () => { throw new Error('VOE Login fehlgeschlagen: Falscher Username oder Passwort');