From 7b480acd104cacf6f4bdc8ad564fb72a429ec19c Mon Sep 17 00:00:00 2001 From: M_Viper Date: Tue, 24 Mar 2026 15:34:44 +0000 Subject: [PATCH] Upload main.js via GUI --- main.js | 392 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 387 insertions(+), 5 deletions(-) diff --git a/main.js b/main.js index cf8a532..2376ca0 100644 --- a/main.js +++ b/main.js @@ -13,6 +13,7 @@ let updater = null; const { createRepoGitHub, createRepoGitea, + checkGiteaConnection, listGiteaRepos, getGiteaRepoContents, getGiteaFileContent, @@ -47,6 +48,162 @@ const IV = Buffer.alloc(16, 0); const DEFAULT_CONCURRENCY = 4; // temp drag cleanup delay (ms) const TMP_CLEANUP_MS = 20_000; +const RETRY_QUEUE_INTERVAL_MS = 15_000; +const RETRY_MAX_ATTEMPTS = 8; + +let retryQueue = []; +let retryQueueRunning = false; +let retryQueueTimer = null; + +function getRetryQueueFilePath() { + return ppath.join(app.getPath('userData'), 'retry-queue.json'); +} + +function readRetryQueueFromDisk() { + try { + const file = getRetryQueueFilePath(); + if (!fs.existsSync(file)) return []; + const parsed = JSON.parse(fs.readFileSync(file, 'utf8')); + return Array.isArray(parsed) ? parsed : []; + } catch (e) { + console.error('readRetryQueueFromDisk error', e); + return []; + } +} + +function saveRetryQueueToDisk() { + try { + const file = getRetryQueueFilePath(); + const dir = app.getPath('userData'); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(file, JSON.stringify(retryQueue, null, 2), 'utf8'); + } catch (e) { + console.error('saveRetryQueueToDisk error', e); + } +} + +function broadcastRetryQueueUpdate(extra = {}) { + const payload = { + size: retryQueue.length, + items: retryQueue.slice(0, 100), + ...extra + }; + for (const win of BrowserWindow.getAllWindows()) { + try { win.webContents.send('retry-queue-updated', payload); } catch (_) {} + } +} + +function isRetryableNetworkError(errorLike) { + const raw = String(errorLike && errorLike.message ? errorLike.message : errorLike || '').toLowerCase(); + if (!raw) return false; + return ( + raw.includes('econnrefused') || + raw.includes('enotfound') || + raw.includes('eai_again') || + raw.includes('getaddrinfo') || + raw.includes('etimedout') || + raw.includes('timeout') || + raw.includes('econnaborted') || + raw.includes('socket hang up') || + raw.includes('503') || + raw.includes('502') || + raw.includes('504') + ); +} + +function enqueueRetryWriteTask(data, reason) { + const item = { + id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, + type: 'write-gitea-file', + payload: { + owner: data.owner, + repo: data.repo, + path: data.path, + content: data.content || '', + ref: data.ref || 'HEAD' + }, + attempts: 0, + nextRetryAt: Date.now() + 5000, + createdAt: new Date().toISOString(), + lastError: String(reason || '') + }; + retryQueue.push(item); + saveRetryQueueToDisk(); + broadcastRetryQueueUpdate({ event: 'queued', item }); + return item; +} + +async function processRetryQueueOnce() { + if (retryQueueRunning) { + return { ok: true, skipped: true, reason: 'already-running', size: retryQueue.length }; + } + + retryQueueRunning = true; + const now = Date.now(); + let processed = 0; + let succeeded = 0; + let failed = 0; + + try { + const credentials = readCredentials(); + const token = credentials && credentials.giteaToken; + const url = credentials && credentials.giteaURL; + + if (!token || !url) { + return { ok: false, error: 'missing-token-or-url', processed: 0, size: retryQueue.length }; + } + + const survivors = []; + for (const item of retryQueue) { + if ((item.nextRetryAt || 0) > now) { + survivors.push(item); + continue; + } + + processed++; + if (item.type !== 'write-gitea-file') { + survivors.push(item); + continue; + } + + try { + const payload = item.payload || {}; + const base64 = Buffer.from(payload.content || '', 'utf8').toString('base64'); + await uploadGiteaFile({ + token, + url, + owner: payload.owner, + repo: payload.repo, + path: payload.path, + contentBase64: base64, + message: `Retry edit ${payload.path} via Git Manager GUI`, + branch: payload.ref || 'HEAD' + }); + succeeded++; + } catch (e) { + const attempts = (item.attempts || 0) + 1; + if (attempts >= RETRY_MAX_ATTEMPTS) { + failed++; + } else { + const backoffMs = Math.min(300000, 5000 * Math.pow(2, attempts)); + survivors.push({ + ...item, + attempts, + nextRetryAt: Date.now() + backoffMs, + lastError: String(e && e.message ? e.message : e) + }); + } + } + } + + retryQueue = survivors; + saveRetryQueueToDisk(); + broadcastRetryQueueUpdate({ event: 'processed', processed, succeeded, failed }); + return { ok: true, processed, succeeded, failed, size: retryQueue.length }; + } finally { + retryQueueRunning = false; + } +} /* ----------------------------- Utilities for safe filesystem ops @@ -115,10 +272,21 @@ function createWindow() { } app.whenReady().then(() => { + retryQueue = readRetryQueueFromDisk(); createWindow(); + broadcastRetryQueueUpdate({ event: 'startup' }); + retryQueueTimer = setInterval(() => { + processRetryQueueOnce().catch(e => console.error('processRetryQueueOnce timer error', e)); + }, RETRY_QUEUE_INTERVAL_MS); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); +app.on('before-quit', () => { + if (retryQueueTimer) { + clearInterval(retryQueueTimer); + retryQueueTimer = null; + } +}); /* ----------------------------- Helper: read credentials @@ -137,6 +305,30 @@ function readCredentials() { } } +function mapIpcError(errorLike) { + const raw = String(errorLike && errorLike.message ? errorLike.message : errorLike || '').toLowerCase(); + if (!raw) return 'Unbekannter Fehler.'; + if (raw.includes('401') || raw.includes('authentifizierung') || raw.includes('unauthorized')) { + return 'Authentifizierung fehlgeschlagen. Bitte Token in den Einstellungen prüfen.'; + } + if (raw.includes('403') || raw.includes('forbidden') || raw.includes('zugriff verweigert')) { + return 'Zugriff verweigert. Bitte Token-Berechtigungen prüfen.'; + } + if (raw.includes('404') || raw.includes('not found') || raw.includes('nicht gefunden')) { + return 'Server oder Ressource nicht gefunden. Bitte URL und Repository prüfen.'; + } + if (raw.includes('econnrefused') || raw.includes('enotfound') || raw.includes('eai_again') || raw.includes('getaddrinfo')) { + return 'Server nicht erreichbar. Bitte DNS, IPv4/IPv6 und Port prüfen.'; + } + 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')) { + return 'Ungültige URL. Beispiel für IPv6: http://[2001:db8::1]:3000'; + } + return String(errorLike && errorLike.message ? errorLike.message : errorLike); +} + /* ----------------------------- Generic concurrency runner (worker pool) ----------------------------- */ @@ -244,7 +436,7 @@ ipcMain.handle('create-repo', async (event, data) => { } else return { ok: false, error: 'unknown-platform' }; } catch (e) { console.error('create-repo error', e); - return { ok: false, error: String(e) }; + return { ok: false, error: mapIpcError(e) }; } }); @@ -312,7 +504,7 @@ ipcMain.handle('push-project', async (event, data) => { return { ok: true }; } catch (e) { console.error('push-project error', e); - return { ok: false, error: String(e) }; + return { ok: false, error: mapIpcError(e) }; } }); @@ -608,7 +800,7 @@ ipcMain.handle('list-gitea-repos', async (event, data) => { return { ok: true, repos }; } catch (e) { console.error('list-gitea-repos error', e); - return { ok: false, error: String(e) }; + return { ok: false, error: mapIpcError(e) }; } }); @@ -630,7 +822,7 @@ ipcMain.handle('get-gitea-repo-contents', async (event, data) => { return { ok: true, items: result.items || result, empty: result.empty || false }; } catch (e) { console.error('get-gitea-repo-contents error', e); - return { ok: false, error: String(e) }; + return { ok: false, error: mapIpcError(e) }; } }); @@ -651,7 +843,7 @@ ipcMain.handle('get-gitea-file-content', async (event, data) => { return { ok: true, content }; } catch (e) { console.error('get-gitea-file-content error', e); - return { ok: false, error: String(e) }; + return { ok: false, error: mapIpcError(e) }; } }); @@ -819,6 +1011,47 @@ ipcMain.handle('write-gitea-file', async (event, data) => { return { ok: true, uploaded }; } catch (e) { console.error('write-gitea-file error', e); + if (isRetryableNetworkError(e)) { + const queued = enqueueRetryWriteTask(data || {}, e && e.message ? e.message : String(e)); + return { + ok: true, + queued: true, + queueId: queued.id, + message: 'Netzwerkproblem erkannt. Änderung wurde in die Retry-Queue gelegt.' + }; + } + return { ok: false, error: String(e) }; + } +}); + +ipcMain.handle('get-retry-queue', async () => { + try { + return { ok: true, size: retryQueue.length, items: retryQueue.slice(0, 100) }; + } catch (e) { + return { ok: false, error: String(e) }; + } +}); + +ipcMain.handle('process-retry-queue-now', async () => { + try { + return await processRetryQueueOnce(); + } catch (e) { + return { ok: false, error: String(e) }; + } +}); + +ipcMain.handle('remove-retry-queue-item', async (event, data) => { + try { + const id = data && data.id; + if (!id) return { ok: false, error: 'missing-id' }; + const before = retryQueue.length; + retryQueue = retryQueue.filter(item => item.id !== id); + if (retryQueue.length !== before) { + saveRetryQueueToDisk(); + broadcastRetryQueueUpdate({ event: 'removed', id }); + } + return { ok: true, size: retryQueue.length }; + } catch (e) { return { ok: false, error: String(e) }; } }); @@ -1397,6 +1630,135 @@ ipcMain.handle('upload-and-push', async (event, data) => { return { ok: false, error: String(e) }; } }); + +ipcMain.handle('run-batch-repo-action', async (event, data) => { + try { + const credentials = readCredentials(); + if (!credentials || !credentials.giteaToken || !credentials.giteaURL) { + return { ok: false, error: 'missing-token-or-url' }; + } + + const action = (data && data.action) || 'refresh'; + const rawRepos = Array.isArray(data && data.repos) ? data.repos : []; + const options = (data && data.options) || {}; + + const repos = rawRepos + .map(r => String(r || '').trim()) + .filter(Boolean) + .map(v => { + const [owner, repo] = v.split('/'); + return { owner, repo, id: `${owner}/${repo}` }; + }) + .filter(r => r.owner && r.repo); + + if (repos.length === 0) { + return { ok: false, error: 'no-valid-repositories' }; + } + + const cloneBaseUrl = String(credentials.giteaURL || '').replace(/\/$/, ''); + const token = credentials.giteaToken; + + const sendProgress = (payload) => { + try { event.sender.send('batch-action-progress', payload); } catch (_) {} + }; + + const results = []; + for (let i = 0; i < repos.length; i++) { + const item = repos[i]; + const progressBase = { index: i + 1, total: repos.length, repo: item.id, action }; + sendProgress({ ...progressBase, status: 'running' }); + + try { + if (action === 'refresh') { + await getGiteaRepoContents({ + token, + url: credentials.giteaURL, + owner: item.owner, + repo: item.repo, + path: '', + ref: 'HEAD' + }); + results.push({ repo: item.id, ok: true, message: 'Repository aktualisiert' }); + } else if (action === 'clone') { + const targetDir = options.cloneTargetDir; + if (!targetDir) throw new Error('clone-target-missing'); + const repoDir = ppath.join(targetDir, item.repo); + if (fs.existsSync(repoDir)) throw new Error(`target-exists: ${repoDir}`); + + const cloneUrl = `${cloneBaseUrl}/${item.owner}/${item.repo}.git`; + let authCloneUrl = cloneUrl; + try { + const u = new URL(cloneUrl); + if (u.protocol.startsWith('http')) { + u.username = encodeURIComponent(token); + authCloneUrl = u.toString(); + } + } catch (_) {} + + execSync(`git clone --depth 1 \"${authCloneUrl}\" \"${repoDir}\"`, { stdio: 'ignore' }); + results.push({ repo: item.id, ok: true, message: `Geklont nach ${repoDir}` }); + } else if (action === 'create-tag') { + const tag = String(options.tag || '').trim(); + if (!tag) throw new Error('tag-missing'); + await createGiteaRelease({ + token, + url: credentials.giteaURL, + owner: item.owner, + repo: item.repo, + data: { + tag_name: tag, + name: tag, + body: options.body || '', + draft: false, + prerelease: !!options.prerelease, + target_commitish: options.target_commitish || 'HEAD' + } + }); + results.push({ repo: item.id, ok: true, message: `Tag erstellt: ${tag}` }); + } else if (action === 'create-release') { + const tag = String(options.tag || '').trim(); + if (!tag) throw new Error('tag-missing'); + const name = String(options.name || tag).trim(); + await createGiteaRelease({ + token, + url: credentials.giteaURL, + owner: item.owner, + repo: item.repo, + data: { + tag_name: tag, + name, + body: options.body || '', + draft: !!options.draft, + prerelease: !!options.prerelease, + target_commitish: options.target_commitish || 'HEAD' + } + }); + results.push({ repo: item.id, ok: true, message: `Release erstellt: ${name}` }); + } else { + throw new Error(`unknown-action: ${action}`); + } + + sendProgress({ ...progressBase, status: 'ok' }); + } catch (e) { + const msg = mapIpcError(e); + results.push({ repo: item.id, ok: false, error: msg }); + sendProgress({ ...progressBase, status: 'error', error: msg }); + } + } + + const success = results.filter(r => r.ok).length; + const failed = results.length - success; + return { + ok: true, + action, + summary: { total: results.length, success, failed }, + results + }; + } catch (e) { + console.error('run-batch-repo-action error', e); + return { ok: false, error: String(e) }; + } +}); /* ================================ RENAME / CREATE / MOVE HANDLERS ================================ */ @@ -2025,4 +2387,24 @@ ipcMain.handle('start-update-download', async (event, asset) => { console.error('[Main] Download-Fehler:', error); return { ok: false, error: String(error) }; } +}); + +ipcMain.handle('test-gitea-connection', async (event, data) => { + try { + const credentials = readCredentials(); + const token = (data && data.token) || (credentials && credentials.giteaToken); + const url = (data && data.url) || (credentials && credentials.giteaURL); + if (!url) return { ok: false, error: 'Gitea URL fehlt.' }; + + const result = await checkGiteaConnection({ + token, + url, + timeout: (data && data.timeout) || 8000 + }); + + return { ok: result.ok, result }; + } catch (e) { + console.error('test-gitea-connection error', e); + return { ok: false, error: mapIpcError(e) }; + } }); \ No newline at end of file