Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f11730fc7 | |||
| e0827faf42 | |||
| 1041f39ced | |||
| 7a43d24a32 | |||
| d6968a4954 | |||
| 464d15464a | |||
| da47343b2e | |||
| d41865608f | |||
| e4b1215aa7 | |||
| 1d7b5e8d6e | |||
| e79c0f411d | |||
| 9da186e5d2 |
274
main.js
274
main.js
@@ -6,9 +6,36 @@ const os = require('os');
|
|||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const { execSync, spawnSync } = require('child_process');
|
const { execSync, spawnSync } = require('child_process');
|
||||||
const https = require('https');
|
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;
|
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 {
|
const {
|
||||||
createRepoGitHub,
|
createRepoGitHub,
|
||||||
createRepoGitea,
|
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
|
app / window
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
@@ -614,7 +660,10 @@ function createWindow() {
|
|||||||
win.loadFile(ppath.join(__dirname, 'renderer', 'index.html'));
|
win.loadFile(ppath.join(__dirname, 'renderer', 'index.html'));
|
||||||
// win.webContents.openDevTools();
|
// 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)
|
// Schließen-Button -> Tray statt Beenden (nur wenn Autostart aktiv)
|
||||||
win.on('close', (e) => {
|
win.on('close', (e) => {
|
||||||
@@ -627,12 +676,21 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
retryQueue = readRetryQueueFromDisk();
|
// OPTIMIERUNG: Fenster wird schnell erstellt
|
||||||
createWindow();
|
createWindow();
|
||||||
broadcastRetryQueueUpdate({ event: 'startup' });
|
|
||||||
retryQueueTimer = setInterval(() => {
|
// OPTIMIERUNG: RetryQueue asynchron laden (nicht blockierend)
|
||||||
processRetryQueueOnce().catch(e => console.error('processRetryQueueOnce timer error', e));
|
setImmediate(() => {
|
||||||
}, RETRY_QUEUE_INTERVAL_MS);
|
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('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
|
||||||
});
|
});
|
||||||
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
|
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
|
||||||
@@ -747,8 +805,8 @@ function isSafeGitRef(ref) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeGitRef(ref, fallback = 'main') {
|
function sanitizeGitRef(ref, fallback = 'main') {
|
||||||
const value = String(ref || '').trim();
|
// DEPRECATED: Use normalizeBranch() from helpers instead
|
||||||
return isSafeGitRef(value) ? value : fallback;
|
return normalizeBranch(ref, 'gitea');
|
||||||
}
|
}
|
||||||
|
|
||||||
function runGitSync(args, cwd, options = {}) {
|
function runGitSync(args, cwd, options = {}) {
|
||||||
@@ -818,11 +876,12 @@ ipcMain.handle('save-credentials', async (event, data) => {
|
|||||||
const CREDENTIALS_FILE = getCredentialsFilePath();
|
const CREDENTIALS_FILE = getCredentialsFilePath();
|
||||||
persistCredentials(data);
|
persistCredentials(data);
|
||||||
|
|
||||||
console.log('✅ Credentials gespeichert in:', CREDENTIALS_FILE);
|
logger.info('save-credentials', 'Credentials saved successfully', { file: CREDENTIALS_FILE });
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('save-credentials error', e);
|
const errInfo = formatErrorForUser(e, 'save-credentials');
|
||||||
return { ok: false, error: String(e) };
|
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 level = String(payload?.level || 'log').toLowerCase();
|
||||||
const message = String(payload?.message || 'renderer-log');
|
const message = String(payload?.message || 'renderer-log');
|
||||||
const details = payload?.payload;
|
const details = payload?.payload;
|
||||||
const fn = level === 'error' ? console.error : (level === 'warn' ? console.warn : console.log);
|
logger[level === 'error' ? 'error' : (level === 'warn' ? 'warn' : 'info')]('renderer', message, details);
|
||||||
fn('[UPLOAD_DEBUG][renderer->main]', message, details || '');
|
|
||||||
} catch (e) {
|
} 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 {
|
try {
|
||||||
return readCredentials();
|
return readCredentials();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('load-credentials', e);
|
logger.error('load-credentials', 'Failed to load credentials', { error: e.message });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -854,17 +912,45 @@ ipcMain.handle('get-credentials-status', async () => {
|
|||||||
return getCredentialReadStatus();
|
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 () => {
|
ipcMain.handle('get-gitea-current-user', async () => {
|
||||||
try {
|
try {
|
||||||
const creds = readCredentials();
|
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 });
|
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 };
|
return { ok: true, user };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errMsg = e.response?.data?.message || e.response?.data || e.message;
|
const errInfo = formatErrorForUser(e, 'get-gitea-current-user');
|
||||||
const errStatus = e.response?.status;
|
logger.error('get-gitea-current-user', errInfo.technicalMessage, errInfo.details);
|
||||||
console.error('get-gitea-current-user error', errStatus, errMsg);
|
return { ok: false, error: errInfo.userMessage };
|
||||||
return { ok: false, error: `(${errStatus || '?'}) ${errMsg}` };
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -988,7 +1074,7 @@ ipcMain.handle('migrate-repo-to-gitea', async (event, data) => {
|
|||||||
ipcMain.handle('create-repo', async (event, data) => {
|
ipcMain.handle('create-repo', async (event, data) => {
|
||||||
try {
|
try {
|
||||||
const credentials = readCredentials();
|
const credentials = readCredentials();
|
||||||
if (!credentials) return { ok: false, error: 'no-credentials' };
|
if (!credentials) return { ok: false, error: 'Keine Zugangsdaten gespeichert' };
|
||||||
|
|
||||||
if (data.platform === 'github') {
|
if (data.platform === 'github') {
|
||||||
const repo = await createRepoGitHub({
|
const repo = await createRepoGitHub({
|
||||||
@@ -998,8 +1084,12 @@ ipcMain.handle('create-repo', async (event, data) => {
|
|||||||
license: data.license || '',
|
license: data.license || '',
|
||||||
private: data.private || false
|
private: data.private || false
|
||||||
});
|
});
|
||||||
|
logger.info('create-repo', `GitHub repo created: ${data.name}`);
|
||||||
return { ok: true, repo };
|
return { ok: true, repo };
|
||||||
} else if (data.platform === 'gitea') {
|
} else if (data.platform === 'gitea') {
|
||||||
|
// Cache invalidieren nach Repo-Erstellung
|
||||||
|
caches.repos.clear();
|
||||||
|
|
||||||
const repo = await createRepoGitea({
|
const repo = await createRepoGitea({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
token: credentials.giteaToken,
|
token: credentials.giteaToken,
|
||||||
@@ -1008,11 +1098,15 @@ ipcMain.handle('create-repo', async (event, data) => {
|
|||||||
license: data.license || '',
|
license: data.license || '',
|
||||||
private: data.private || false
|
private: data.private || false
|
||||||
});
|
});
|
||||||
|
logger.info('create-repo', `Gitea repo created: ${data.name}`);
|
||||||
return { ok: true, repo };
|
return { ok: true, repo };
|
||||||
} else return { ok: false, error: 'unknown-platform' };
|
} else {
|
||||||
|
return { ok: false, error: 'Plattform nicht unterstützt' };
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('create-repo error', e);
|
const errInfo = formatErrorForUser(e, 'create-repo');
|
||||||
return { ok: false, error: mapIpcError(e) };
|
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;
|
const folder = data && data.folder;
|
||||||
if (!folder || !fs.existsSync(folder)) return { ok: false, error: 'folder-not-found' };
|
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 };
|
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);
|
const tree = buildTree(folder, opts);
|
||||||
|
caches.fileTree.set(cacheKey, tree);
|
||||||
|
|
||||||
return { ok: true, tree };
|
return { ok: true, tree };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('getFileTree error', e);
|
console.error('getFileTree error', e);
|
||||||
@@ -1378,12 +1483,15 @@ ipcMain.handle('deleteFile', async (event, data) => {
|
|||||||
const protocol = urlObj.protocol === 'https:' ? https : http;
|
const protocol = urlObj.protocol === 'https:' ? https : http;
|
||||||
const port = urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80);
|
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
|
// Helper: GET contents from Gitea API
|
||||||
function giteaGet(path) {
|
function giteaGet(path) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const req = protocol.request({
|
const req = protocol.request({
|
||||||
hostname: urlObj.hostname, port,
|
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',
|
method: 'GET',
|
||||||
headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' }
|
headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' }
|
||||||
}, (res) => {
|
}, (res) => {
|
||||||
@@ -1404,7 +1512,7 @@ ipcMain.handle('deleteFile', async (event, data) => {
|
|||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
message: `Delete ${filePath} via Git Manager GUI`,
|
message: `Delete ${filePath} via Git Manager GUI`,
|
||||||
sha,
|
sha,
|
||||||
branch: data.ref || 'HEAD'
|
branch: useBranch
|
||||||
});
|
});
|
||||||
const req = protocol.request({
|
const req = protocol.request({
|
||||||
hostname: urlObj.hostname, port,
|
hostname: urlObj.hostname, port,
|
||||||
@@ -1459,20 +1567,24 @@ ipcMain.handle('deleteFile', async (event, data) => {
|
|||||||
return { ok: false, error: 'Keine Dateien zum Löschen gefunden' };
|
return { ok: false, error: 'Keine Dateien zum Löschen gefunden' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all files sequentially
|
// OPTIMIERUNG: Delete all files in parallel (up to 4 concurrent)
|
||||||
let failed = 0;
|
const deleteOps = filesToDelete.map(f =>
|
||||||
for (const f of filesToDelete) {
|
async () => {
|
||||||
const res = await giteaDeleteFile(f.path, f.sha);
|
const res = await giteaDeleteFile(f.path, f.sha);
|
||||||
if (!res.ok) {
|
return { path: f.path, ...res };
|
||||||
console.error(`Fehler beim Löschen von ${f.path}:`, res.error);
|
|
||||||
failed++;
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
|
|
||||||
|
const deleteResults = await runParallel(deleteOps, 4);
|
||||||
|
const failed = deleteResults.filter(r => !r.ok).length;
|
||||||
|
|
||||||
if (failed > 0) {
|
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` };
|
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 };
|
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 token = (data && data.token) || (credentials && credentials.giteaToken);
|
||||||
const url = (data && data.url) || (credentials && credentials.giteaURL);
|
const url = (data && data.url) || (credentials && credentials.giteaURL);
|
||||||
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
|
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 });
|
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) {
|
} 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) };
|
return { ok: false, error: mapIpcError(e) };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1708,54 +1833,76 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
|
|||||||
const credentials = readCredentials();
|
const credentials = readCredentials();
|
||||||
const owner = data.owner;
|
const owner = data.owner;
|
||||||
const repo = data.repo;
|
const repo = data.repo;
|
||||||
console.log('[UPLOAD_DEBUG][main] upload-gitea-file:start', {
|
|
||||||
|
logger.info('upload-gitea-file', 'Upload started', {
|
||||||
uploadDebugId,
|
uploadDebugId,
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
platform: data.platform || 'gitea',
|
platform: data.platform || 'gitea',
|
||||||
destPath: data.destPath || '',
|
files: Array.isArray(data.localPath) ? data.localPath.length : (data.localPath ? 1 : 0)
|
||||||
branch: data.branch || 'HEAD',
|
|
||||||
localPathCount: Array.isArray(data.localPath) ? data.localPath.length : (data.localPath ? 1 : 0)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// GitHub upload path
|
// GitHub upload path
|
||||||
if (data.platform === 'github') {
|
if (data.platform === 'github') {
|
||||||
const githubToken = (data.token) || (credentials && credentials.githubToken);
|
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 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 message = data.message || 'Upload via Git Manager GUI';
|
||||||
const localFiles = Array.isArray(data.localPath) ? data.localPath : (data.localPath ? [data.localPath] : []);
|
const localFiles = Array.isArray(data.localPath) ? data.localPath : (data.localPath ? [data.localPath] : []);
|
||||||
const results = [];
|
const results = [];
|
||||||
for (const localFile of localFiles) {
|
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 raw = fs.readFileSync(localFile);
|
||||||
const base64 = raw.toString('base64');
|
const base64 = raw.toString('base64');
|
||||||
const fileName = ppath.basename(localFile);
|
const fileName = ppath.basename(localFile);
|
||||||
const targetPath = destPath ? `${destPath}/${fileName}` : fileName;
|
const targetPath = destPath ? `${destPath}/${fileName}` : fileName;
|
||||||
try {
|
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 });
|
results.push({ file: localFile, ok: true, uploaded });
|
||||||
|
logger.debug('upload-gitea-file', `File uploaded: ${fileName}`);
|
||||||
} catch (e) {
|
} 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;
|
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 };
|
return { ok: failedCount === 0, results, failedCount, debugId: uploadDebugId };
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = (data && data.token) || (credentials && credentials.giteaToken);
|
const token = (data && data.token) || (credentials && credentials.giteaToken);
|
||||||
const url = (data && data.url) || (credentials && credentials.giteaURL);
|
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;
|
const owner2 = owner;
|
||||||
// destPath is the target folder in the repo
|
|
||||||
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
|
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
|
||||||
// Branch wird unverändert übernommen (main UND master werden unterstützt)
|
let branch = normalizeBranch(data.branch || 'HEAD', 'gitea');
|
||||||
let branch = sanitizeGitRef(data.branch || 'HEAD', 'HEAD');
|
|
||||||
const message = data.message || 'Upload via Git Manager GUI';
|
const message = data.message || 'Upload via Git Manager GUI';
|
||||||
const localFiles = Array.isArray(data.localPath) ? data.localPath : (data.localPath ? [data.localPath] : []);
|
const localFiles = Array.isArray(data.localPath) ? data.localPath : (data.localPath ? [data.localPath] : []);
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
for (const localFile of localFiles) {
|
for (const localFile of localFiles) {
|
||||||
if (!fs.existsSync(localFile)) {
|
if (!fs.existsSync(localFile)) {
|
||||||
results.push({ file: localFile, ok: false, error: 'local-file-not-found' });
|
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 base64 = raw.toString('base64');
|
||||||
const fileName = ppath.basename(localFile);
|
const fileName = ppath.basename(localFile);
|
||||||
|
|
||||||
// FIXED: Handle destPath correctly. Always combine destPath + filename.
|
|
||||||
let targetPath;
|
let targetPath;
|
||||||
if (destPath && destPath.length > 0) {
|
if (destPath && destPath.length > 0) {
|
||||||
targetPath = `${destPath}/${fileName}`;
|
targetPath = `${destPath}/${fileName}`;
|
||||||
@@ -1785,13 +1931,16 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
|
|||||||
branch
|
branch
|
||||||
});
|
});
|
||||||
results.push({ file: localFile, ok: true, uploaded });
|
results.push({ file: localFile, ok: true, uploaded });
|
||||||
|
logger.debug('upload-gitea-file', `File uploaded: ${fileName}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('upload error for', localFile, e);
|
const errInfo = formatErrorForUser(e, 'File upload');
|
||||||
results.push({ file: localFile, ok: false, error: String(e) });
|
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;
|
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 };
|
return { ok: failedCount === 0, results, failedCount, debugId: uploadDebugId };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[UPLOAD_DEBUG][main] upload-gitea-file:fatal', { uploadDebugId, error: String(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) {
|
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 = [];
|
const files = [];
|
||||||
if (Array.isArray(r.body)) {
|
if (Array.isArray(r.body)) {
|
||||||
for (const item of r.body) {
|
for (const item of r.body) {
|
||||||
@@ -2728,21 +2877,21 @@ ipcMain.handle('rename-gitea-item', async (event, data) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function readFileContent(filePath) {
|
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, '') : '';
|
return r.body?.content ? r.body.content.replace(/\n/g, '') : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadFile(targetPath, contentBase64, message) {
|
async function uploadFile(targetPath, contentBase64, message) {
|
||||||
// Check if exists first (need SHA for update)
|
// 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 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: 'HEAD' };
|
const body = { message, content: contentBase64, branch: 'main' };
|
||||||
if (check.body?.sha) body.sha = check.body.sha;
|
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);
|
return giteaRequest('POST', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${targetPath.split('/').map(encodeURIComponent).join('/')}`, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteFile(filePath, sha) {
|
async function deleteFile(filePath, sha) {
|
||||||
return giteaRequest('DELETE', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${filePath.split('/').map(encodeURIComponent).join('/')}`, {
|
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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2782,8 +2931,11 @@ ipcMain.handle('create-gitea-item', async (event, data) => {
|
|||||||
const targetPath = type === 'folder' ? `${itemPath}/.gitkeep` : itemPath;
|
const targetPath = type === 'folder' ? `${itemPath}/.gitkeep` : itemPath;
|
||||||
const content = Buffer.from('').toString('base64');
|
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) => {
|
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({
|
const req = protocol.request({
|
||||||
hostname: urlObj.hostname, port,
|
hostname: urlObj.hostname, port,
|
||||||
path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${targetPath.split('/').map(encodeURIComponent).join('/')}`,
|
path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${targetPath.split('/').map(encodeURIComponent).join('/')}`,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "git-manager-gui",
|
"name": "git-manager-gui",
|
||||||
"version": "2.0.6",
|
"version": "2.0.9",
|
||||||
"description": "Git Manager GUI - Verwaltung von Git Repositories",
|
"description": "Git Manager GUI - Verwaltung von Git Repositories",
|
||||||
"author": "M_Viper",
|
"author": "M_Viper",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
|
|||||||
@@ -175,5 +175,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
// Utility
|
// Utility
|
||||||
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),
|
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),
|
||||||
openExternalUrl: (url) => ipcRenderer.invoke('open-external-url', url),
|
openExternalUrl: (url) => ipcRenderer.invoke('open-external-url', url),
|
||||||
debugToMain: (level, message, payload) => ipcRenderer.send('renderer-debug-log', { level, message, payload })
|
debugToMain: (level, message, payload) => ipcRenderer.send('renderer-debug-log', { level, message, payload }),
|
||||||
|
|
||||||
|
// Debugging & Diagnostics
|
||||||
|
getDebugInfo: () => ipcRenderer.invoke('get-debug-info'),
|
||||||
|
clearCache: (type) => ipcRenderer.invoke('clear-cache', type || 'all')
|
||||||
});
|
});
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
<span class="toolbar-kicker">Workspace Control</span>
|
<span class="toolbar-kicker">Workspace Control</span>
|
||||||
<strong>Git Manager Explorer Pro</strong>
|
<strong>Git Manager Explorer Pro</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<span id="project-toolbar-title" class="toolbar-project-title"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar-top-actions">
|
<div class="toolbar-top-actions">
|
||||||
@@ -84,6 +85,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="project-gravur-bar"><span id="project-gravur-title" class="project-gravur-title"></span></div>
|
||||||
|
<div class="project-gravur-separator"></div>
|
||||||
<div id="contentArea" class="content-area">
|
<div id="contentArea" class="content-area">
|
||||||
<aside id="favHistorySidebar" class="fav-history-sidebar" aria-label="Favoriten und Verlauf"></aside>
|
<aside id="favHistorySidebar" class="fav-history-sidebar" aria-label="Favoriten und Verlauf"></aside>
|
||||||
<main id="main">
|
<main id="main">
|
||||||
|
|||||||
@@ -3362,6 +3362,11 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadRepoContents(owner, repo, path) {
|
async function loadRepoContents(owner, repo, path) {
|
||||||
|
// Projekttitel als Gravur unterhalb der Toolbar setzen (nur Repo-Name)
|
||||||
|
const gravurTitle = document.getElementById('project-gravur-title');
|
||||||
|
if (gravurTitle) {
|
||||||
|
gravurTitle.textContent = repo;
|
||||||
|
}
|
||||||
currentState.view = 'gitea-repo';
|
currentState.view = 'gitea-repo';
|
||||||
currentState.owner = owner;
|
currentState.owner = owner;
|
||||||
currentState.repo = repo;
|
currentState.repo = repo;
|
||||||
@@ -3403,9 +3408,26 @@ async function loadRepoContents(owner, repo, path) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
// Bei Fehler Gravur zurücksetzen
|
||||||
|
const gravurTitle = document.getElementById('project-gravur-title');
|
||||||
|
if (gravurTitle) {
|
||||||
|
gravurTitle.textContent = '';
|
||||||
|
}
|
||||||
showError('Error: ' + (res.error || 'Unknown error'));
|
showError('Error: ' + (res.error || 'Unknown error'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Wenn zur Übersicht gewechselt wird, Gravur zurücksetzen
|
||||||
|
function resetProjectGravurTitle() {
|
||||||
|
const gravurTitle = document.getElementById('project-gravur-title');
|
||||||
|
if (gravurTitle) gravurTitle.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nach dem Laden der Repo-Liste oder beim Klick auf "Zurück" rufe resetProjectGravurTitle() auf
|
||||||
|
const origLoadRepos = loadRepos;
|
||||||
|
loadRepos = function(...args) {
|
||||||
|
resetProjectGravurTitle();
|
||||||
|
return origLoadRepos.apply(this, args);
|
||||||
|
};
|
||||||
|
|
||||||
const grid = $('explorerGrid');
|
const grid = $('explorerGrid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
|
|||||||
@@ -1,3 +1,44 @@
|
|||||||
|
.project-gravur-bar {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
background: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.project-gravur-title {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(174, 189, 216, 0.18);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
font-style: italic;
|
||||||
|
user-select: none;
|
||||||
|
text-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 1.5px 0 rgba(0,0,0,0.13);
|
||||||
|
transition: color 0.2s;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.project-gravur-separator {
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, rgba(88,213,255,0.10) 0%, rgba(92,135,255,0.10) 100%);
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
.toolbar-project-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(174, 189, 216, 0.22);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-left: 32px;
|
||||||
|
font-style: italic;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
text-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 1.5px 0 rgba(0,0,0,0.13);
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
:root {
|
:root {
|
||||||
/* Moderne Farbpalette */
|
/* Moderne Farbpalette */
|
||||||
--bg-primary: #07111f;
|
--bg-primary: #07111f;
|
||||||
@@ -96,12 +137,15 @@
|
|||||||
|
|
||||||
.titlebar-strip-title {
|
.titlebar-strip-title {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: rgba(174, 189, 216, 0.68);
|
color: rgba(174, 189, 216, 0.32); /* viel dezenter */
|
||||||
letter-spacing: 0.045em;
|
letter-spacing: 0.045em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
text-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 1.5px 0 rgba(0,0,0,0.13);
|
||||||
|
font-style: italic;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#titlebar-strip .win-controls {
|
#titlebar-strip .win-controls {
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -94,7 +94,8 @@ class Updater {
|
|||||||
const ext = process.platform === 'win32' ? '.exe' : '.AppImage';
|
const ext = process.platform === 'win32' ? '.exe' : '.AppImage';
|
||||||
return assets.find(a => {
|
return assets.find(a => {
|
||||||
const name = String(a?.name || '').toLowerCase();
|
const name = String(a?.name || '').toLowerCase();
|
||||||
const validName = /^[a-z0-9._-]+$/.test(name);
|
// Leerzeichen im Namen erlauben!
|
||||||
|
const validName = /^[a-z0-9._\- ]+$/i.test(name);
|
||||||
return validName && name.endsWith(ext);
|
return validName && name.endsWith(ext);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user