diff --git a/main.js b/main.js index 0b4b60a..f5abff5 100644 --- a/main.js +++ b/main.js @@ -283,7 +283,7 @@ function tryReadWrappedCredentials(raw) { credentials: null, needsRewrite: false, reason: 'safeStorage-unavailable', - message: 'Gespeicherte Zugangsdaten koennen in dieser Sitzung nicht gelesen werden. Bitte neu anmelden.' + message: 'Gespeicherte Zugangsdaten können in dieser Sitzung nicht gelesen werden. Bitte neu anmelden.' }; } @@ -299,7 +299,7 @@ function tryReadWrappedCredentials(raw) { credentials: null, needsRewrite: false, reason: 'safeStorage-decrypt-failed', - message: 'Gespeicherte Zugangsdaten konnten nicht entschluesselt werden. Bitte Token neu eingeben.' + message: 'Gespeicherte Zugangsdaten konnten nicht entschlüsselt werden. Bitte Token neu eingeben.' }; } } @@ -349,7 +349,7 @@ function readCredentialsFromFile(filePath) { credentials: null, needsRewrite: false, reason: 'legacy-decrypt-failed', - message: 'Gespeicherte Zugangsdaten waren unlesbar und wurden zurueckgesetzt. Bitte Zugangsdaten neu eingeben.', + message: 'Gespeicherte Zugangsdaten waren unlesbar und wurden zurückgesetzt. Bitte Zugangsdaten neu eingeben.', filePath }; } @@ -678,7 +678,7 @@ function createWindow() { } }); - // Externe Fenster immer blockieren und nur erlaubte URLs explizit im System-Browser oeffnen. + // Externe Fenster immer blockieren und nur erlaubte URLs explizit im System-Browser öffnen. win.webContents.setWindowOpenHandler(({ url }) => { if (isAllowedExternalUrl(url)) { shell.openExternal(url).catch(() => {}); @@ -833,7 +833,7 @@ function mapIpcError(errorLike) { if (raw.includes('timeout') || raw.includes('econnaborted')) { return 'Zeitüberschreitung bei der Verbindung. Bitte Netzwerk oder Server prüfen.'; } - if (raw.includes('http://') || raw.includes('https://') || raw.includes('ungueltige gitea url') || raw.includes('ungültige gitea url') || raw.includes('invalid')) { + if (raw.includes('http://') || raw.includes('https://') || raw.includes('ungültige gitea url') || raw.includes('ungültige gitea url') || raw.includes('invalid')) { return 'Ungültige URL. Beispiel für IPv6: http://[2001:db8::1]:3000'; } return String(errorLike && errorLike.message ? errorLike.message : errorLike); @@ -1714,7 +1714,7 @@ ipcMain.handle('deleteFile', async (event, data) => { return files; } - async function executeDeleteBatch(files) { + async function executeDeleteBatch(files, concurrency = 2) { const deleteOps = files.map(f => async () => { const res = await giteaDeleteFile(f.path, f.sha); @@ -1722,8 +1722,8 @@ ipcMain.handle('deleteFile', async (event, data) => { } ); - // Gitea: mit Concurrency=2 + Retry-Logik bei 409-Konflikten für bessere Performance - const deleteResults = await runParallel(deleteOps, 2); + // Concurrency kann dynamisch reduziert werden, um API-Konflikte bei Nachloeschungen zu vermeiden + const deleteResults = await runParallel(deleteOps, concurrency); const failedEntries = deleteResults.filter(r => !r.ok || !(r.result && r.result.ok)); return { deleteResults, failedEntries }; } @@ -1741,38 +1741,31 @@ ipcMain.handle('deleteFile', async (event, data) => { return { ok: true, deleted: 0 }; } - // Delete attempt - let { failedEntries } = await executeDeleteBatch(filesToDelete); + // Delete attempt (schnell) + sequenzieller Fallback bei Konflikten + let { failedEntries } = await executeDeleteBatch(filesToDelete, 2); + // Bei Teilfehlern sofort sequenziellen Retry fahren (stabiler bei parallelen Commits) if (failedEntries.length > 0) { const failedFiles = failedEntries .map(r => (r.result && r.result.path) ? r.result.path : 'unknown') .join(', '); - logger.error('deleteFile', `Failed to delete ${failedEntries.length} files`, { files: failedFiles }); - return { ok: false, error: `${failedEntries.length} von ${filesToDelete.length} Dateien konnten nicht gelöscht werden` }; + logger.warn('deleteFile', `Initial delete had ${failedEntries.length} failures, retrying sequentially`, { files: failedFiles }); + } + + // Verification + bis zu zwei sequenzielle Nachlöschversuche auf verbleibende Dateien + for (let attempt = 0; attempt < 2; attempt++) { + if (!(await pathStillExists(filePath))) break; + const remaining = await collectAllFiles(filePath); + if (remaining.length === 0) break; + ({ failedEntries } = await executeDeleteBatch(remaining, 1)); } - // Verification + one automatic retry on leftovers if (await pathStillExists(filePath)) { const remaining = await collectAllFiles(filePath); - if (remaining.length > 0) { - ({ failedEntries } = await executeDeleteBatch(remaining)); - if (failedEntries.length > 0) { - const failedFiles = failedEntries - .map(r => (r.result && r.result.path) ? r.result.path : 'unknown') - .join(', '); - return { - ok: false, - error: `Nachloeschung fehlgeschlagen. ${failedEntries.length} Dateien konnten nicht gelöscht werden (${failedFiles}).` - }; - } - } - } - - if (await pathStillExists(filePath)) { + const remainingPreview = remaining.slice(0, 10).map(f => f.path).join(', '); return { ok: false, - error: 'Löschen unvollständig: Der Ordnerpfad ist nach zwei Versuchen noch vorhanden.' + error: `Löschen unvollständig: ${remaining.length} Dateien sind noch vorhanden (${remainingPreview}${remaining.length > 10 ? ', ...' : ''}).` }; } @@ -3844,7 +3837,7 @@ ipcMain.handle('import-settings-bundle', async () => { const parsed = JSON.parse(raw); if (!parsed || parsed.bundleType !== 'git-manager-settings-backup') { - return { ok: false, error: 'Ungueltige Backup-Datei.' }; + return { ok: false, error: 'Ungültige Backup-Datei.' }; } const existing = readCredentials() || {};