From d6968a49540341672249d7a2e0720c45603f9de1 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Wed, 1 Apr 2026 19:30:30 +0000 Subject: [PATCH] Upload via Git Manager GUI - main.js --- main.js | 274 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 213 insertions(+), 61 deletions(-) diff --git a/main.js b/main.js index d3aec4e..258e217 100644 --- a/main.js +++ b/main.js @@ -6,9 +6,36 @@ const os = require('os'); const crypto = require('crypto'); const { execSync, spawnSync } = require('child_process'); const https = require('https'); -const Updater = require('./updater.js'); // Auto-Updater + +// IMPORTS: Zentrale Utilities +const { + logger, + normalizeBranch, + parseApiError, + formatErrorForUser, + caches, + runParallel, + retryWithBackoff +} = require('./src/utils/helpers.js'); + +// OPTIMIERUNG: Updater wird nicht sofort geladen, sondern verzögert nach Startup +let Updater = null; let updater = null; +// Updater lazy-loaded nach 2 Sekunden +function initUpdaterAsync() { + setTimeout(() => { + try { + if (!Updater) Updater = require('./updater.js'); + if (Updater && !updater) { + updater = new Updater(); + } + } catch (e) { + console.warn('Updater init deferred error:', e.message); + } + }, 2000); +} + const { createRepoGitHub, createRepoGitea, @@ -480,6 +507,25 @@ function getSafeTmpDir(baseName) { } +/* ----------------------------- + Startup-Optimierung + ----------------------------- */ + +// OPTIMIERUNG: V8 Code Caching aktivieren +app.commandLine.appendSwitch('enable-v8-code-caching'); + +// OPTIMIERUNG: GPU-Beschleunigung aktivieren (für bessere Performance) +if (os.platform() !== 'linux') { + app.commandLine.appendSwitch('enable-gpu-acceleration'); + app.commandLine.appendSwitch('enable-gpu-compositing'); +} + +// OPTIMIERUNG: Native Binaries mit Memory-Mapping +app.commandLine.appendSwitch('v8-cache-options', 'code'); + +// OPTIMIERUNG: Speicher-Handling optimieren +app.commandLine.appendSwitch('enable-memory-coordination'); + /* ----------------------------- app / window ----------------------------- */ @@ -614,7 +660,10 @@ function createWindow() { win.loadFile(ppath.join(__dirname, 'renderer', 'index.html')); // win.webContents.openDevTools(); - createTray(win); + // OPTIMIERUNG: Tray wird verzögert hergestellt (nicht beim Fenster-Create) + setImmediate(() => { + createTray(win); + }); // Schließen-Button -> Tray statt Beenden (nur wenn Autostart aktiv) win.on('close', (e) => { @@ -627,12 +676,21 @@ function createWindow() { } app.whenReady().then(() => { - retryQueue = readRetryQueueFromDisk(); + // OPTIMIERUNG: Fenster wird schnell erstellt createWindow(); - broadcastRetryQueueUpdate({ event: 'startup' }); - retryQueueTimer = setInterval(() => { - processRetryQueueOnce().catch(e => console.error('processRetryQueueOnce timer error', e)); - }, RETRY_QUEUE_INTERVAL_MS); + + // OPTIMIERUNG: RetryQueue asynchron laden (nicht blockierend) + setImmediate(() => { + retryQueue = readRetryQueueFromDisk(); + broadcastRetryQueueUpdate({ event: 'startup' }); + retryQueueTimer = setInterval(() => { + processRetryQueueOnce().catch(e => console.error('processRetryQueueOnce timer error', e)); + }, RETRY_QUEUE_INTERVAL_MS); + }); + + // OPTIMIERUNG: Updater wird verzögert geladen (nach Fenster erstellt) + initUpdaterAsync(); + app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); @@ -747,8 +805,8 @@ function isSafeGitRef(ref) { } function sanitizeGitRef(ref, fallback = 'main') { - const value = String(ref || '').trim(); - return isSafeGitRef(value) ? value : fallback; + // DEPRECATED: Use normalizeBranch() from helpers instead + return normalizeBranch(ref, 'gitea'); } function runGitSync(args, cwd, options = {}) { @@ -818,11 +876,12 @@ ipcMain.handle('save-credentials', async (event, data) => { const CREDENTIALS_FILE = getCredentialsFilePath(); persistCredentials(data); - console.log('✅ Credentials gespeichert in:', CREDENTIALS_FILE); + logger.info('save-credentials', 'Credentials saved successfully', { file: CREDENTIALS_FILE }); return { ok: true }; } catch (e) { - console.error('save-credentials error', e); - return { ok: false, error: String(e) }; + const errInfo = formatErrorForUser(e, 'save-credentials'); + logger.error('save-credentials', errInfo.technicalMessage, errInfo.details); + return { ok: false, error: errInfo.userMessage }; } }); @@ -831,10 +890,9 @@ ipcMain.on('renderer-debug-log', (_event, payload) => { const level = String(payload?.level || 'log').toLowerCase(); const message = String(payload?.message || 'renderer-log'); const details = payload?.payload; - const fn = level === 'error' ? console.error : (level === 'warn' ? console.warn : console.log); - fn('[UPLOAD_DEBUG][renderer->main]', message, details || ''); + logger[level === 'error' ? 'error' : (level === 'warn' ? 'warn' : 'info')]('renderer', message, details); } catch (e) { - console.warn('[UPLOAD_DEBUG][renderer->main] logging failed', String(e)); + logger.warn('renderer-debug-log', 'Logging failed', { error: e.message }); } }); @@ -842,7 +900,7 @@ ipcMain.handle('load-credentials', async () => { try { return readCredentials(); } catch (e) { - console.error('load-credentials', e); + logger.error('load-credentials', 'Failed to load credentials', { error: e.message }); return null; } }); @@ -854,17 +912,45 @@ ipcMain.handle('get-credentials-status', async () => { return getCredentialReadStatus(); }); +// Debug-API für die Anwendung +ipcMain.handle('get-debug-info', async () => { + return { + version: app.getVersion(), + logs: logger.getRecent(30), + cacheStats: { + repos: caches.repos.size(), + fileTree: caches.fileTree.size(), + api: caches.api.size() + } + }; +}); + +ipcMain.handle('clear-cache', async (event, type = 'all') => { + try { + if (type === 'all' || type === 'repos') caches.repos.clear(); + if (type === 'all' || type === 'fileTree') caches.fileTree.clear(); + if (type === 'all' || type === 'api') caches.api.clear(); + logger.info('clear-cache', `Cache cleared: ${type}`); + return { ok: true, message: `Cache gelöscht: ${type}` }; + } catch (e) { + logger.error('clear-cache', 'Failed to clear cache', { error: e.message }); + return { ok: false, error: String(e) }; + } +}); + ipcMain.handle('get-gitea-current-user', async () => { try { const creds = readCredentials(); - if (!creds?.giteaToken || !creds?.giteaURL) return { ok: false, error: 'no-credentials' }; + if (!creds?.giteaToken || !creds?.giteaURL) { + return { ok: false, error: 'no-credentials' }; + } const user = await getGiteaCurrentUser({ token: creds.giteaToken, url: creds.giteaURL }); + logger.info('get-gitea-current-user', 'User loaded', { user: user?.login }); return { ok: true, user }; } catch (e) { - const errMsg = e.response?.data?.message || e.response?.data || e.message; - const errStatus = e.response?.status; - console.error('get-gitea-current-user error', errStatus, errMsg); - return { ok: false, error: `(${errStatus || '?'}) ${errMsg}` }; + const errInfo = formatErrorForUser(e, 'get-gitea-current-user'); + logger.error('get-gitea-current-user', errInfo.technicalMessage, errInfo.details); + return { ok: false, error: errInfo.userMessage }; } }); @@ -988,7 +1074,7 @@ ipcMain.handle('migrate-repo-to-gitea', async (event, data) => { ipcMain.handle('create-repo', async (event, data) => { try { const credentials = readCredentials(); - if (!credentials) return { ok: false, error: 'no-credentials' }; + if (!credentials) return { ok: false, error: 'Keine Zugangsdaten gespeichert' }; if (data.platform === 'github') { const repo = await createRepoGitHub({ @@ -998,8 +1084,12 @@ ipcMain.handle('create-repo', async (event, data) => { license: data.license || '', private: data.private || false }); + logger.info('create-repo', `GitHub repo created: ${data.name}`); return { ok: true, repo }; } else if (data.platform === 'gitea') { + // Cache invalidieren nach Repo-Erstellung + caches.repos.clear(); + const repo = await createRepoGitea({ name: data.name, token: credentials.giteaToken, @@ -1008,11 +1098,15 @@ ipcMain.handle('create-repo', async (event, data) => { license: data.license || '', private: data.private || false }); + logger.info('create-repo', `Gitea repo created: ${data.name}`); return { ok: true, repo }; - } else return { ok: false, error: 'unknown-platform' }; + } else { + return { ok: false, error: 'Plattform nicht unterstützt' }; + } } catch (e) { - console.error('create-repo error', e); - return { ok: false, error: mapIpcError(e) }; + const errInfo = formatErrorForUser(e, 'create-repo'); + logger.error('create-repo', errInfo.technicalMessage, errInfo.details); + return { ok: false, error: errInfo.userMessage }; } }); @@ -1298,7 +1392,18 @@ ipcMain.handle('getFileTree', async (event, data) => { const folder = data && data.folder; if (!folder || !fs.existsSync(folder)) return { ok: false, error: 'folder-not-found' }; const opts = { exclude: (data && data.exclude) || ['node_modules'], maxDepth: (data && data.maxDepth) || 10 }; + + // OPTIMIERUNG: Cache für File-Trees (5 min) + const cacheKey = `fileTree:${folder}:${JSON.stringify(opts)}`; + const cached = caches.fileTree.get(cacheKey); + if (cached) { + logger.debug('getFileTree', 'Cache hit', { folder }); + return { ok: true, tree: cached, cached: true }; + } + const tree = buildTree(folder, opts); + caches.fileTree.set(cacheKey, tree); + return { ok: true, tree }; } catch (e) { console.error('getFileTree error', e); @@ -1378,12 +1483,15 @@ ipcMain.handle('deleteFile', async (event, data) => { const protocol = urlObj.protocol === 'https:' ? https : http; const port = urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80); + // HEAD-Auflösung: Wenn ref === 'HEAD', zu Fallback konvertieren (wird gleich wie GitHub behandelt) + let useBranch = (data.ref && data.ref !== 'HEAD') ? data.ref : 'main'; + // Helper: GET contents from Gitea API function giteaGet(path) { return new Promise((resolve, reject) => { const req = protocol.request({ hostname: urlObj.hostname, port, - path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${path.split('/').map(encodeURIComponent).join('/')}?ref=${data.ref || 'HEAD'}`, + path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${path.split('/').map(encodeURIComponent).join('/')}?ref=${useBranch}`, method: 'GET', headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' } }, (res) => { @@ -1404,7 +1512,7 @@ ipcMain.handle('deleteFile', async (event, data) => { const body = JSON.stringify({ message: `Delete ${filePath} via Git Manager GUI`, sha, - branch: data.ref || 'HEAD' + branch: useBranch }); const req = protocol.request({ hostname: urlObj.hostname, port, @@ -1459,20 +1567,24 @@ ipcMain.handle('deleteFile', async (event, data) => { return { ok: false, error: 'Keine Dateien zum Löschen gefunden' }; } - // Delete all files sequentially - let failed = 0; - for (const f of filesToDelete) { - const res = await giteaDeleteFile(f.path, f.sha); - if (!res.ok) { - console.error(`Fehler beim Löschen von ${f.path}:`, res.error); - failed++; + // OPTIMIERUNG: Delete all files in parallel (up to 4 concurrent) + const deleteOps = filesToDelete.map(f => + async () => { + const res = await giteaDeleteFile(f.path, f.sha); + return { path: f.path, ...res }; } - } + ); + + const deleteResults = await runParallel(deleteOps, 4); + const failed = deleteResults.filter(r => !r.ok).length; if (failed > 0) { + const failedFiles = deleteResults.filter(r => !r.ok).map(r => r.path).join(', '); + logger.error('deleteFile', `Failed to delete ${failed} files`, { files: failedFiles }); return { ok: false, error: `${failed} von ${filesToDelete.length} Dateien konnten nicht gelöscht werden` }; } + logger.info('deleteFile', `Deleted ${filesToDelete.length} files successfully`); return { ok: true, deleted: filesToDelete.length }; } @@ -1577,10 +1689,23 @@ ipcMain.handle('get-gitea-repo-contents', async (event, data) => { const token = (data && data.token) || (credentials && credentials.giteaToken); const url = (data && data.url) || (credentials && credentials.giteaURL); if (!token || !url) return { ok: false, error: 'missing-token-or-url' }; + + // OPTIMIERUNG: Cache für Repo-Inhalte (5 min) + const cacheKey = `gitea:${owner}/${repo}:${p}:${ref || 'HEAD'}`; + const cached = caches.repos.get(cacheKey); + if (cached) { + logger.debug('ipc-get-repo-contents', 'Cache hit', { cacheKey }); + return { ok: true, ...cached }; + } + const result = await getGiteaRepoContents({ token, url, owner, repo, path: p, ref }); - return { ok: true, items: result.items || result, empty: result.empty || false }; + const response = { items: result.items || result, empty: result.empty || false }; + caches.repos.set(cacheKey, response); + + return { ok: true, ...response }; } catch (e) { - console.error('get-gitea-repo-contents error', e); + const errInfo = formatErrorForUser(e, 'get-gitea-repo-contents'); + logger.error('get-gitea-repo-contents', errInfo.technicalMessage, errInfo.details); return { ok: false, error: mapIpcError(e) }; } }); @@ -1708,54 +1833,76 @@ ipcMain.handle('upload-gitea-file', async (event, data) => { const credentials = readCredentials(); const owner = data.owner; const repo = data.repo; - console.log('[UPLOAD_DEBUG][main] upload-gitea-file:start', { + + logger.info('upload-gitea-file', 'Upload started', { uploadDebugId, owner, repo, platform: data.platform || 'gitea', - destPath: data.destPath || '', - branch: data.branch || 'HEAD', - localPathCount: Array.isArray(data.localPath) ? data.localPath.length : (data.localPath ? 1 : 0) + files: Array.isArray(data.localPath) ? data.localPath.length : (data.localPath ? 1 : 0) }); // GitHub upload path if (data.platform === 'github') { const githubToken = (data.token) || (credentials && credentials.githubToken); - if (!githubToken) return { ok: false, error: `GitHub Token fehlt. (${uploadDebugId})` }; + if (!githubToken) { + logger.warn('upload-gitea-file', 'GitHub token missing'); + return { ok: false, error: `GitHub Token fehlt.` }; + } const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, ''); - const branch = (data.branch && data.branch !== 'HEAD') ? data.branch : 'main'; + const branch = normalizeBranch(data.branch, 'github'); const message = data.message || 'Upload via Git Manager GUI'; const localFiles = Array.isArray(data.localPath) ? data.localPath : (data.localPath ? [data.localPath] : []); const results = []; for (const localFile of localFiles) { - if (!fs.existsSync(localFile)) { results.push({ file: localFile, ok: false, error: 'local-file-not-found' }); continue; } + if (!fs.existsSync(localFile)) { + results.push({ file: localFile, ok: false, error: 'local-file-not-found' }); + continue; + } const raw = fs.readFileSync(localFile); const base64 = raw.toString('base64'); const fileName = ppath.basename(localFile); const targetPath = destPath ? `${destPath}/${fileName}` : fileName; try { - const uploaded = await uploadGithubFile({ token: githubToken, owner, repo, path: targetPath, contentBase64: base64, message: `${message} - ${fileName}`, branch }); + const uploaded = await uploadGithubFile({ + token: githubToken, + owner, + repo, + path: targetPath, + contentBase64: base64, + message: `${message} - ${fileName}`, + branch + }); results.push({ file: localFile, ok: true, uploaded }); + logger.debug('upload-gitea-file', `File uploaded: ${fileName}`); } catch (e) { - results.push({ file: localFile, ok: false, error: String(e) }); + const errInfo = formatErrorForUser(e, 'File upload'); + results.push({ file: localFile, ok: false, error: errInfo.userMessage }); + logger.error('upload-gitea-file', `Upload failed for ${fileName}`, errInfo.details); } } const failedCount = results.filter(r => !r.ok).length; - console.log('[UPLOAD_DEBUG][main] upload-gitea-file:github-done', { uploadDebugId, failedCount, total: results.length }); + logger.info('upload-gitea-file', `GitHub upload done`, { failedCount, total: results.length, uploadDebugId }); return { ok: failedCount === 0, results, failedCount, debugId: uploadDebugId }; } const token = (data && data.token) || (credentials && credentials.giteaToken); const url = (data && data.url) || (credentials && credentials.giteaURL); - if (!token || !url) return { ok: false, error: `missing-token-or-url (${uploadDebugId})` }; + if (!token || !url) { + logger.warn('upload-gitea-file', 'Missing token or URL'); + return { ok: false, error: `Zugangsdaten fehlen` }; + } + + // Invalid cache for repo after upload + caches.repos.invalidate(`${owner}/${repo}`); + const owner2 = owner; - // destPath is the target folder in the repo const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, ''); - // Branch wird unverändert übernommen (main UND master werden unterstützt) - let branch = sanitizeGitRef(data.branch || 'HEAD', 'HEAD'); + let branch = normalizeBranch(data.branch || 'HEAD', 'gitea'); const message = data.message || 'Upload via Git Manager GUI'; const localFiles = Array.isArray(data.localPath) ? data.localPath : (data.localPath ? [data.localPath] : []); const results = []; + for (const localFile of localFiles) { if (!fs.existsSync(localFile)) { results.push({ file: localFile, ok: false, error: 'local-file-not-found' }); @@ -1765,7 +1912,6 @@ ipcMain.handle('upload-gitea-file', async (event, data) => { const base64 = raw.toString('base64'); const fileName = ppath.basename(localFile); - // FIXED: Handle destPath correctly. Always combine destPath + filename. let targetPath; if (destPath && destPath.length > 0) { targetPath = `${destPath}/${fileName}`; @@ -1785,13 +1931,16 @@ ipcMain.handle('upload-gitea-file', async (event, data) => { branch }); results.push({ file: localFile, ok: true, uploaded }); + logger.debug('upload-gitea-file', `File uploaded: ${fileName}`); } catch (e) { - console.error('upload error for', localFile, e); - results.push({ file: localFile, ok: false, error: String(e) }); + const errInfo = formatErrorForUser(e, 'File upload'); + results.push({ file: localFile, ok: false, error: errInfo.userMessage }); + logger.error('upload-gitea-file', `Upload failed for ${fileName}`, errInfo.details); } } + const failedCount = results.filter(r => !r.ok).length; - console.log('[UPLOAD_DEBUG][main] upload-gitea-file:gitea-done', { uploadDebugId, failedCount, total: results.length }); + logger.info('upload-gitea-file', 'Gitea upload done', { failedCount, total: results.length, uploadDebugId }); return { ok: failedCount === 0, results, failedCount, debugId: uploadDebugId }; } catch (e) { console.error('[UPLOAD_DEBUG][main] upload-gitea-file:fatal', { uploadDebugId, error: String(e) }); @@ -2714,7 +2863,7 @@ ipcMain.handle('rename-gitea-item', async (event, data) => { } async function collectFiles(path) { - const r = await giteaRequest('GET', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${path.split('/').map(encodeURIComponent).join('/')}?ref=HEAD`, null); + const r = await giteaRequest('GET', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${path.split('/').map(encodeURIComponent).join('/')}?ref=main`, null); const files = []; if (Array.isArray(r.body)) { for (const item of r.body) { @@ -2728,21 +2877,21 @@ ipcMain.handle('rename-gitea-item', async (event, data) => { } async function readFileContent(filePath) { - const r = await giteaRequest('GET', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${filePath.split('/').map(encodeURIComponent).join('/')}?ref=HEAD`, null); + const r = await giteaRequest('GET', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${filePath.split('/').map(encodeURIComponent).join('/')}?ref=main`, null); return r.body?.content ? r.body.content.replace(/\n/g, '') : ''; } async function uploadFile(targetPath, contentBase64, message) { // Check if exists first (need SHA for update) - const check = await giteaRequest('GET', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${targetPath.split('/').map(encodeURIComponent).join('/')}?ref=HEAD`, null); - const body = { message, content: contentBase64, branch: 'HEAD' }; + const check = await giteaRequest('GET', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${targetPath.split('/').map(encodeURIComponent).join('/')}?ref=main`, null); + const body = { message, content: contentBase64, branch: 'main' }; if (check.body?.sha) body.sha = check.body.sha; return giteaRequest('POST', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${targetPath.split('/').map(encodeURIComponent).join('/')}`, body); } async function deleteFile(filePath, sha) { return giteaRequest('DELETE', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${filePath.split('/').map(encodeURIComponent).join('/')}`, { - message: `Delete ${filePath} (rename)`, sha, branch: 'HEAD' + message: `Delete ${filePath} (rename)`, sha, branch: 'main' }); } @@ -2781,9 +2930,12 @@ ipcMain.handle('create-gitea-item', async (event, data) => { // Für Ordner: .gitkeep Datei anlegen const targetPath = type === 'folder' ? `${itemPath}/.gitkeep` : itemPath; const content = Buffer.from('').toString('base64'); + + // Branch-Auflösung: HEAD zu 'main' konvertieren + const branch = (data.branch && data.branch !== 'HEAD') ? data.branch : 'main'; return new Promise((resolve) => { - const body = JSON.stringify({ message: `Create ${itemPath}`, content, branch: data.branch || 'HEAD' }); + const body = JSON.stringify({ message: `Create ${itemPath}`, content, branch }); const req = protocol.request({ hostname: urlObj.hostname, port, path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${targetPath.split('/').map(encodeURIComponent).join('/')}`,