fix: continue VOE account fallback rotation
CI / verify (push) Canceled after 0s

This commit is contained in:
Sucukdeluxe
2026-09-07 02:44:08 +02:00
parent 0f5cb68308
commit c8838d34b3
5 changed files with 62 additions and 3 deletions
+6 -2
View File
@@ -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. - 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. - 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. - 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. - 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. - 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. - 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 ## Offene nächste Schritte
- Keine offenen Schritte für Release `v2.1.43`; Rollback-Ziel ist Anwendungsversion `2.1.42`. - 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. - Bei Bedarf einen Arbeitsweg ohne `&` im absoluten Pfad verwenden oder die npm-Aufrufe weiterhin direkt ausführen.
## Zuletzt verifiziert ## Zuletzt verifiziert
Stand: 02.09.2026 Stand: 07.09.2026
- Lint: erfolgreich, 0 Warnungen und 0 Fehler. - Lint: erfolgreich, 0 Warnungen und 0 Fehler.
- Haupttests: 805 erfolgreich, 0 fehlgeschlagen. - Haupttests: 807 erfolgreich, 0 fehlgeschlagen.
- Backup-API-Tests: 17 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. - 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. - 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. - Release-Commit `5311cdf` und der annotierte Tag `v2.1.43` wurden auf GitHub und Forgejo mit identischen Commit-Hashes verifiziert.
+1
View File
@@ -42,6 +42,7 @@ function classifyAccountFailure(error) {
const cooldownPatterns = [ const cooldownPatterns = [
/\b429\b|rate[- ]?limit|too many requests/i, /\b429\b|rate[- ]?limit|too many requests/i,
/quota|not enough (disk )?(space|storage)|insufficient (disk )?space/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, /disk (space )?full|storage (exhausted|full|voll|limit)|account (full|voll)/i,
/session (expired|abgelaufen)|CSRF[- ]?Token nicht gefunden|not logged in/i, /session (expired|abgelaufen)|CSRF[- ]?Token nicht gefunden|not logged in/i,
/Keine Session erhalten|Session konnte nicht verifiziert werden/i, /Keine Session erhalten|Session konnte nicht verifiziert werden/i,
+8 -1
View File
@@ -230,8 +230,8 @@ class UploadManager extends EventEmitter {
_shouldSkipRetryOnAccountError(err) { _shouldSkipRetryOnAccountError(err) {
if (!err) return false; if (!err) return false;
if (err.transientNetwork === true) return false; if (err.transientNetwork === true) return false;
// Explicit account-level flag from hoster parsers — highest priority.
if (err.accountError === true) return true; if (err.accountError === true) return true;
if (classifyAccountFailure(err) !== 'none') return true;
if (!err.message) return false; if (!err.message) return false;
const m = String(err.message); const m = String(err.message);
const PATTERNS = [ const PATTERNS = [
@@ -1109,6 +1109,13 @@ class UploadManager extends EventEmitter {
if (this._isFileRejectedError(err)) break; if (this._isFileRejectedError(err)) break;
if (this._isHosterTransientError(err)) break; if (this._isHosterTransientError(err)) break;
if (this._isTransientNetworkError(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; if (attempt >= maxAttempts) break;
} }
} }
+1
View File
@@ -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'); const quota = new Error('not enough disk space on your account');
quota.accountError = true; quota.accountError = true;
assert.equal(classifyAccountFailure(quota), 'cooldown'); 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'); assert.equal(classifyAccountFailure(new Error('HTTP 429 Too Many Requests')), 'cooldown');
const transient = new Error('HTTP 503 Service Unavailable'); const transient = new Error('HTTP 503 Service Unavailable');
transient.transientNetwork = true; transient.transientNetwork = true;
+46
View File
@@ -1111,6 +1111,13 @@ describe('UploadManager', () => {
'must not match generic "lehnte Datei ab" as file-rejected'); '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', () => { it('keeps true file rejections as file-rejected', () => {
const mgr = new UploadManager({}); const mgr = new UploadManager({});
const err = new Error('Byse lehnte Datei ab: Duplicate'); const err = new Error('Byse lehnte Datei ab: Duplicate');
@@ -1169,6 +1176,45 @@ describe('UploadManager', () => {
assert.deepEqual(successes, [{ hoster: 'byse.sx', accountId: 'fallback' }]); 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 () => { it('emits a manual account pause for credential failures', async () => {
mockUploadFile.mock.mockImplementation(async () => { mockUploadFile.mock.mockImplementation(async () => {
throw new Error('VOE Login fehlgeschlagen: Falscher Username oder Passwort'); throw new Error('VOE Login fehlgeschlagen: Falscher Username oder Passwort');