Update from Git Manager GUI
This commit is contained in:
@@ -682,9 +682,29 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
|
|||||||
repo = parts[1];
|
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';
|
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 () => {
|
const fetchSha = async () => {
|
||||||
try {
|
try {
|
||||||
const existing = await getGiteaRepoContents({ token, url: base, owner, repo, path, ref: branchName });
|
const existing = await getGiteaRepoContents({ token, url: base, owner, repo, path, ref: branchName });
|
||||||
|
|||||||
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