/** * Gemeinsame Utility-Funktionen für Git Manager GUI * - Branch Handling * - API Error Handling * - Standardisiertes Logging * - Caching */ const fs = require('fs'); const ppath = require('path'); // ===== LOGGING SYSTEM ===== const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 }; let currentLogLevel = process.env.NODE_ENV === 'production' ? LOG_LEVELS.INFO : LOG_LEVELS.DEBUG; let logQueue = []; const MAX_LOG_BUFFER = 2000; function formatLog(level, context, message, details = null) { const timestamp = new Date().toISOString(); const levelStr = Object.keys(LOG_LEVELS).find(k => LOG_LEVELS[k] === level); return { timestamp, level: levelStr, context, message, details, pid: process.pid }; } function writeLog(logEntry) { logQueue.push(logEntry); if (logQueue.length > MAX_LOG_BUFFER) { logQueue.shift(); } // Auch in Console schreiben const { level, timestamp, context, message, details } = logEntry; const prefix = `[${timestamp}] [${level}] [${context}]`; if (level === 'ERROR' && details?.error) { console.error(prefix, message, details.error); } else if (level === 'WARN') { console.warn(prefix, message, details ? JSON.stringify(details) : ''); } else if (level !== 'DEBUG' || process.env.DEBUG) { console.log(prefix, message, details ? JSON.stringify(details) : ''); } } const logger = { debug: (context, message, details) => writeLog(formatLog(LOG_LEVELS.DEBUG, context, message, details)), info: (context, message, details) => writeLog(formatLog(LOG_LEVELS.INFO, context, message, details)), warn: (context, message, details) => writeLog(formatLog(LOG_LEVELS.WARN, context, message, details)), error: (context, message, details) => writeLog(formatLog(LOG_LEVELS.ERROR, context, message, details)), getRecent: (count = 20) => logQueue.slice(-count), setLevel: (level) => { currentLogLevel = LOG_LEVELS[level] || LOG_LEVELS.INFO; } }; // ===== BRANCH HANDLING ===== const BRANCH_DEFAULTS = { gitea: 'main', github: 'main' }; function normalizeBranch(branch = 'HEAD', platform = 'gitea') { const value = String(branch || '').trim(); // HEAD sollte immer zu Standard konvertiert werden if (value.toLowerCase() === 'head') { return BRANCH_DEFAULTS[platform] || 'main'; } // Validierung: nur sichere Git-Referenzen if (/^[a-zA-Z0-9._\-/]+$/.test(value)) { return value; } return BRANCH_DEFAULTS[platform] || 'main'; } function isSafeBranch(branch) { return /^[a-zA-Z0-9._\-/]+$/.test(String(branch || '')); } // ===== ERROR HANDLING ===== const ERROR_CODES = { NETWORK: 'NETWORK_ERROR', AUTH_FAILED: 'AUTH_FAILED', NOT_FOUND: 'NOT_FOUND', VALIDATION: 'VALIDATION_ERROR', RATE_LIMIT: 'RATE_LIMIT', SERVER_ERROR: 'SERVER_ERROR', UNKNOWN: 'UNKNOWN_ERROR' }; function parseApiError(error, defaultCode = ERROR_CODES.UNKNOWN) { if (!error) { return { code: defaultCode, message: 'Unknown error', statusCode: null }; } // Axios-style error if (error.response) { const status = error.response.status; const data = error.response.data; let code = defaultCode; if (status === 401 || status === 403) { code = ERROR_CODES.AUTH_FAILED; } else if (status === 404) { code = ERROR_CODES.NOT_FOUND; } else if (status === 429) { code = ERROR_CODES.RATE_LIMIT; } else if (status >= 500) { code = ERROR_CODES.SERVER_ERROR; } return { code, message: data?.message || error.message || `HTTP ${status}`, statusCode: status, rawMessage: data?.message }; } // Network error if (error.message?.includes('timeout') || error.code?.includes('TIMEOUT')) { return { code: ERROR_CODES.NETWORK, message: 'Request timeout', statusCode: null }; } if (error.code?.includes('ECONNREFUSED') || error.message?.includes('ECONNREFUSED')) { return { code: ERROR_CODES.NETWORK, message: 'Connection refused', statusCode: null }; } return { code: ERROR_CODES.UNKNOWN, message: error.message || String(error), statusCode: null }; } function formatErrorForUser(error, context = 'Operation') { const parsed = parseApiError(error); const messages = { [ERROR_CODES.AUTH_FAILED]: `Authentifizierung fehlgeschlagen. Bitte Token überprüfen.`, [ERROR_CODES.NOT_FOUND]: `Ressource nicht gefunden.`, [ERROR_CODES.NETWORK]: `Netzwerkfehler. Bitte Verbindung überprüfen.`, [ERROR_CODES.RATE_LIMIT]: `Zu viele Anfragen. Bitte später versuchen.`, [ERROR_CODES.SERVER_ERROR]: `Server-Fehler. Bitte später versuchen.`, [ERROR_CODES.UNKNOWN]: `${context} fehlgeschlagen.` }; return { userMessage: messages[parsed.code], technicalMessage: parsed.message, code: parsed.code, details: parsed }; } // ===== CACHING SYSTEM ===== class Cache { constructor(ttl = 300000) { // 5 min default this.store = new Map(); this.ttl = ttl; } set(key, value, customTtl = null) { const expiry = Date.now() + (customTtl || this.ttl); this.store.set(key, { value, expiry }); } get(key) { const item = this.store.get(key); if (!item) return null; if (Date.now() > item.expiry) { this.store.delete(key); return null; } return item.value; } invalidate(keyPattern) { for (const [key] of this.store) { if (key.includes(keyPattern)) { this.store.delete(key); } } } clear() { this.store.clear(); } size() { return this.store.size; } } // Standard Caches const caches = { repos: new Cache(600000), // 10 min fileTree: new Cache(300000), // 5 min api: new Cache(120000) // 2 min }; // ===== PARALLEL OPERATIONS ===== async function runParallel(operations, concurrency = 4, onProgress = null) { const results = new Array(operations.length); let completed = 0; let index = 0; async function worker() { while (index < operations.length) { const i = index++; try { results[i] = { ok: true, result: await operations[i]() }; } catch (e) { results[i] = { ok: false, error: e }; } completed++; if (onProgress) { try { onProgress(completed, operations.length); } catch (_) {} } } } const workers = Array.from({ length: Math.min(concurrency, operations.length) }, () => worker()); await Promise.all(workers); return results; } // ===== RETRY LOGIC ===== async function retryWithBackoff(fn, maxAttempts = 3, baseDelay = 1000) { let lastError; for (let attempt = 0; attempt < maxAttempts; attempt++) { try { return await fn(); } catch (e) { lastError = e; if (attempt < maxAttempts - 1) { const delay = baseDelay * Math.pow(2, attempt); await new Promise(resolve => setTimeout(resolve, delay)); } } } throw lastError; } // ===== FILE OPERATIONS ===== function ensureDirectory(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } function safeReadFile(filePath, defaultValue = null) { try { if (!fs.existsSync(filePath)) return defaultValue; return fs.readFileSync(filePath, 'utf8'); } catch (e) { logger.warn('safeReadFile', `Failed to read ${filePath}`, { error: e.message }); return defaultValue; } } function safeWriteFile(filePath, content) { try { ensureDirectory(ppath.dirname(filePath)); fs.writeFileSync(filePath, content, 'utf8'); return true; } catch (e) { logger.error('safeWriteFile', `Failed to write ${filePath}`, { error: e.message }); return false; } } // ===== EXPORTS ===== module.exports = { logger, normalizeBranch, isSafeBranch, parseApiError, formatErrorForUser, ERROR_CODES, Cache, caches, runParallel, retryWithBackoff, ensureDirectory, safeReadFile, safeWriteFile, LOG_LEVELS };