Update from Git Manager GUI
This commit is contained in:
@@ -682,8 +682,28 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
|
||||
repo = parts[1];
|
||||
}
|
||||
|
||||
// Behalte den branch so wie übergeben - keine Konvertierung
|
||||
// Behalte den branch so wie übergeben - aber 'HEAD' muss zum echten Branch aufgelöst werden
|
||||
let branchName = branch || 'HEAD';
|
||||
|
||||
// HEAD-Auflösung: Wenn branch === 'HEAD', den Default-Branch des Repos abrufen
|
||||
if (branchName === 'HEAD') {
|
||||
try {
|
||||
const repoInfoUrl = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
||||
const repoInfo = await tryRequest(repoInfoUrl, token);
|
||||
if (repoInfo.ok && repoInfo.data.default_branch) {
|
||||
branchName = repoInfo.data.default_branch;
|
||||
console.log(`[Upload Debug] HEAD aufgelöst zu: ${branchName}`);
|
||||
} else {
|
||||
// Fallback auf 'main' wenn Auflösung fehlschlägt
|
||||
branchName = 'main';
|
||||
console.warn(`[Upload Debug] HEAD-Auflösung fehlgeschlagen, verwende Fallback: ${branchName}`);
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback auf 'main' wenn Fehler
|
||||
branchName = 'main';
|
||||
console.warn(`[Upload Debug] HEAD-Auflösung fehlgeschlagen (${e.message}), verwende Fallback: ${branchName}`);
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSha = async () => {
|
||||
try {
|
||||
|
||||
293
src/utils/helpers.js
Normal file
293
src/utils/helpers.js
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* 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 = 100;
|
||||
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user