Upload main.js via GUI

This commit is contained in:
2026-03-24 15:34:44 +00:00
parent 658b29368b
commit 7b480acd10

392
main.js
View File

@@ -13,6 +13,7 @@ let updater = null;
const { const {
createRepoGitHub, createRepoGitHub,
createRepoGitea, createRepoGitea,
checkGiteaConnection,
listGiteaRepos, listGiteaRepos,
getGiteaRepoContents, getGiteaRepoContents,
getGiteaFileContent, getGiteaFileContent,
@@ -47,6 +48,162 @@ const IV = Buffer.alloc(16, 0);
const DEFAULT_CONCURRENCY = 4; const DEFAULT_CONCURRENCY = 4;
// temp drag cleanup delay (ms) // temp drag cleanup delay (ms)
const TMP_CLEANUP_MS = 20_000; const TMP_CLEANUP_MS = 20_000;
const RETRY_QUEUE_INTERVAL_MS = 15_000;
const RETRY_MAX_ATTEMPTS = 8;
let retryQueue = [];
let retryQueueRunning = false;
let retryQueueTimer = null;
function getRetryQueueFilePath() {
return ppath.join(app.getPath('userData'), 'retry-queue.json');
}
function readRetryQueueFromDisk() {
try {
const file = getRetryQueueFilePath();
if (!fs.existsSync(file)) return [];
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
return Array.isArray(parsed) ? parsed : [];
} catch (e) {
console.error('readRetryQueueFromDisk error', e);
return [];
}
}
function saveRetryQueueToDisk() {
try {
const file = getRetryQueueFilePath();
const dir = app.getPath('userData');
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(file, JSON.stringify(retryQueue, null, 2), 'utf8');
} catch (e) {
console.error('saveRetryQueueToDisk error', e);
}
}
function broadcastRetryQueueUpdate(extra = {}) {
const payload = {
size: retryQueue.length,
items: retryQueue.slice(0, 100),
...extra
};
for (const win of BrowserWindow.getAllWindows()) {
try { win.webContents.send('retry-queue-updated', payload); } catch (_) {}
}
}
function isRetryableNetworkError(errorLike) {
const raw = String(errorLike && errorLike.message ? errorLike.message : errorLike || '').toLowerCase();
if (!raw) return false;
return (
raw.includes('econnrefused') ||
raw.includes('enotfound') ||
raw.includes('eai_again') ||
raw.includes('getaddrinfo') ||
raw.includes('etimedout') ||
raw.includes('timeout') ||
raw.includes('econnaborted') ||
raw.includes('socket hang up') ||
raw.includes('503') ||
raw.includes('502') ||
raw.includes('504')
);
}
function enqueueRetryWriteTask(data, reason) {
const item = {
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
type: 'write-gitea-file',
payload: {
owner: data.owner,
repo: data.repo,
path: data.path,
content: data.content || '',
ref: data.ref || 'HEAD'
},
attempts: 0,
nextRetryAt: Date.now() + 5000,
createdAt: new Date().toISOString(),
lastError: String(reason || '')
};
retryQueue.push(item);
saveRetryQueueToDisk();
broadcastRetryQueueUpdate({ event: 'queued', item });
return item;
}
async function processRetryQueueOnce() {
if (retryQueueRunning) {
return { ok: true, skipped: true, reason: 'already-running', size: retryQueue.length };
}
retryQueueRunning = true;
const now = Date.now();
let processed = 0;
let succeeded = 0;
let failed = 0;
try {
const credentials = readCredentials();
const token = credentials && credentials.giteaToken;
const url = credentials && credentials.giteaURL;
if (!token || !url) {
return { ok: false, error: 'missing-token-or-url', processed: 0, size: retryQueue.length };
}
const survivors = [];
for (const item of retryQueue) {
if ((item.nextRetryAt || 0) > now) {
survivors.push(item);
continue;
}
processed++;
if (item.type !== 'write-gitea-file') {
survivors.push(item);
continue;
}
try {
const payload = item.payload || {};
const base64 = Buffer.from(payload.content || '', 'utf8').toString('base64');
await uploadGiteaFile({
token,
url,
owner: payload.owner,
repo: payload.repo,
path: payload.path,
contentBase64: base64,
message: `Retry edit ${payload.path} via Git Manager GUI`,
branch: payload.ref || 'HEAD'
});
succeeded++;
} catch (e) {
const attempts = (item.attempts || 0) + 1;
if (attempts >= RETRY_MAX_ATTEMPTS) {
failed++;
} else {
const backoffMs = Math.min(300000, 5000 * Math.pow(2, attempts));
survivors.push({
...item,
attempts,
nextRetryAt: Date.now() + backoffMs,
lastError: String(e && e.message ? e.message : e)
});
}
}
}
retryQueue = survivors;
saveRetryQueueToDisk();
broadcastRetryQueueUpdate({ event: 'processed', processed, succeeded, failed });
return { ok: true, processed, succeeded, failed, size: retryQueue.length };
} finally {
retryQueueRunning = false;
}
}
/* ----------------------------- /* -----------------------------
Utilities for safe filesystem ops Utilities for safe filesystem ops
@@ -115,10 +272,21 @@ function createWindow() {
} }
app.whenReady().then(() => { app.whenReady().then(() => {
retryQueue = readRetryQueueFromDisk();
createWindow(); createWindow();
broadcastRetryQueueUpdate({ event: 'startup' });
retryQueueTimer = setInterval(() => {
processRetryQueueOnce().catch(e => console.error('processRetryQueueOnce timer error', e));
}, RETRY_QUEUE_INTERVAL_MS);
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(); });
app.on('before-quit', () => {
if (retryQueueTimer) {
clearInterval(retryQueueTimer);
retryQueueTimer = null;
}
});
/* ----------------------------- /* -----------------------------
Helper: read credentials Helper: read credentials
@@ -137,6 +305,30 @@ function readCredentials() {
} }
} }
function mapIpcError(errorLike) {
const raw = String(errorLike && errorLike.message ? errorLike.message : errorLike || '').toLowerCase();
if (!raw) return 'Unbekannter Fehler.';
if (raw.includes('401') || raw.includes('authentifizierung') || raw.includes('unauthorized')) {
return 'Authentifizierung fehlgeschlagen. Bitte Token in den Einstellungen prüfen.';
}
if (raw.includes('403') || raw.includes('forbidden') || raw.includes('zugriff verweigert')) {
return 'Zugriff verweigert. Bitte Token-Berechtigungen prüfen.';
}
if (raw.includes('404') || raw.includes('not found') || raw.includes('nicht gefunden')) {
return 'Server oder Ressource nicht gefunden. Bitte URL und Repository prüfen.';
}
if (raw.includes('econnrefused') || raw.includes('enotfound') || raw.includes('eai_again') || raw.includes('getaddrinfo')) {
return 'Server nicht erreichbar. Bitte DNS, IPv4/IPv6 und Port prüfen.';
}
if (raw.includes('timeout') || raw.includes('econnaborted')) {
return 'Zeitüberschreitung bei der Verbindung. Bitte Netzwerk oder Server prüfen.';
}
if (raw.includes('http://') || raw.includes('https://') || raw.includes('ungueltige gitea url') || raw.includes('ungültige gitea url') || raw.includes('invalid')) {
return 'Ungültige URL. Beispiel für IPv6: http://[2001:db8::1]:3000';
}
return String(errorLike && errorLike.message ? errorLike.message : errorLike);
}
/* ----------------------------- /* -----------------------------
Generic concurrency runner (worker pool) Generic concurrency runner (worker pool)
----------------------------- */ ----------------------------- */
@@ -244,7 +436,7 @@ ipcMain.handle('create-repo', async (event, data) => {
} else return { ok: false, error: 'unknown-platform' }; } else return { ok: false, error: 'unknown-platform' };
} catch (e) { } catch (e) {
console.error('create-repo error', e); console.error('create-repo error', e);
return { ok: false, error: String(e) }; return { ok: false, error: mapIpcError(e) };
} }
}); });
@@ -312,7 +504,7 @@ ipcMain.handle('push-project', async (event, data) => {
return { ok: true }; return { ok: true };
} catch (e) { } catch (e) {
console.error('push-project error', e); console.error('push-project error', e);
return { ok: false, error: String(e) }; return { ok: false, error: mapIpcError(e) };
} }
}); });
@@ -608,7 +800,7 @@ ipcMain.handle('list-gitea-repos', async (event, data) => {
return { ok: true, repos }; return { ok: true, repos };
} catch (e) { } catch (e) {
console.error('list-gitea-repos error', e); console.error('list-gitea-repos error', e);
return { ok: false, error: String(e) }; return { ok: false, error: mapIpcError(e) };
} }
}); });
@@ -630,7 +822,7 @@ ipcMain.handle('get-gitea-repo-contents', async (event, data) => {
return { ok: true, items: result.items || result, empty: result.empty || false }; return { ok: true, items: result.items || result, empty: result.empty || false };
} catch (e) { } catch (e) {
console.error('get-gitea-repo-contents error', e); console.error('get-gitea-repo-contents error', e);
return { ok: false, error: String(e) }; return { ok: false, error: mapIpcError(e) };
} }
}); });
@@ -651,7 +843,7 @@ ipcMain.handle('get-gitea-file-content', async (event, data) => {
return { ok: true, content }; return { ok: true, content };
} catch (e) { } catch (e) {
console.error('get-gitea-file-content error', e); console.error('get-gitea-file-content error', e);
return { ok: false, error: String(e) }; return { ok: false, error: mapIpcError(e) };
} }
}); });
@@ -819,6 +1011,47 @@ ipcMain.handle('write-gitea-file', async (event, data) => {
return { ok: true, uploaded }; return { ok: true, uploaded };
} catch (e) { } catch (e) {
console.error('write-gitea-file error', e); console.error('write-gitea-file error', e);
if (isRetryableNetworkError(e)) {
const queued = enqueueRetryWriteTask(data || {}, e && e.message ? e.message : String(e));
return {
ok: true,
queued: true,
queueId: queued.id,
message: 'Netzwerkproblem erkannt. Änderung wurde in die Retry-Queue gelegt.'
};
}
return { ok: false, error: String(e) };
}
});
ipcMain.handle('get-retry-queue', async () => {
try {
return { ok: true, size: retryQueue.length, items: retryQueue.slice(0, 100) };
} catch (e) {
return { ok: false, error: String(e) };
}
});
ipcMain.handle('process-retry-queue-now', async () => {
try {
return await processRetryQueueOnce();
} catch (e) {
return { ok: false, error: String(e) };
}
});
ipcMain.handle('remove-retry-queue-item', async (event, data) => {
try {
const id = data && data.id;
if (!id) return { ok: false, error: 'missing-id' };
const before = retryQueue.length;
retryQueue = retryQueue.filter(item => item.id !== id);
if (retryQueue.length !== before) {
saveRetryQueueToDisk();
broadcastRetryQueueUpdate({ event: 'removed', id });
}
return { ok: true, size: retryQueue.length };
} catch (e) {
return { ok: false, error: String(e) }; return { ok: false, error: String(e) };
} }
}); });
@@ -1397,6 +1630,135 @@ ipcMain.handle('upload-and-push', async (event, data) => {
return { ok: false, error: String(e) }; return { ok: false, error: String(e) };
} }
}); });
ipcMain.handle('run-batch-repo-action', async (event, data) => {
try {
const credentials = readCredentials();
if (!credentials || !credentials.giteaToken || !credentials.giteaURL) {
return { ok: false, error: 'missing-token-or-url' };
}
const action = (data && data.action) || 'refresh';
const rawRepos = Array.isArray(data && data.repos) ? data.repos : [];
const options = (data && data.options) || {};
const repos = rawRepos
.map(r => String(r || '').trim())
.filter(Boolean)
.map(v => {
const [owner, repo] = v.split('/');
return { owner, repo, id: `${owner}/${repo}` };
})
.filter(r => r.owner && r.repo);
if (repos.length === 0) {
return { ok: false, error: 'no-valid-repositories' };
}
const cloneBaseUrl = String(credentials.giteaURL || '').replace(/\/$/, '');
const token = credentials.giteaToken;
const sendProgress = (payload) => {
try { event.sender.send('batch-action-progress', payload); } catch (_) {}
};
const results = [];
for (let i = 0; i < repos.length; i++) {
const item = repos[i];
const progressBase = { index: i + 1, total: repos.length, repo: item.id, action };
sendProgress({ ...progressBase, status: 'running' });
try {
if (action === 'refresh') {
await getGiteaRepoContents({
token,
url: credentials.giteaURL,
owner: item.owner,
repo: item.repo,
path: '',
ref: 'HEAD'
});
results.push({ repo: item.id, ok: true, message: 'Repository aktualisiert' });
} else if (action === 'clone') {
const targetDir = options.cloneTargetDir;
if (!targetDir) throw new Error('clone-target-missing');
const repoDir = ppath.join(targetDir, item.repo);
if (fs.existsSync(repoDir)) throw new Error(`target-exists: ${repoDir}`);
const cloneUrl = `${cloneBaseUrl}/${item.owner}/${item.repo}.git`;
let authCloneUrl = cloneUrl;
try {
const u = new URL(cloneUrl);
if (u.protocol.startsWith('http')) {
u.username = encodeURIComponent(token);
authCloneUrl = u.toString();
}
} catch (_) {}
execSync(`git clone --depth 1 \"${authCloneUrl}\" \"${repoDir}\"`, { stdio: 'ignore' });
results.push({ repo: item.id, ok: true, message: `Geklont nach ${repoDir}` });
} else if (action === 'create-tag') {
const tag = String(options.tag || '').trim();
if (!tag) throw new Error('tag-missing');
await createGiteaRelease({
token,
url: credentials.giteaURL,
owner: item.owner,
repo: item.repo,
data: {
tag_name: tag,
name: tag,
body: options.body || '',
draft: false,
prerelease: !!options.prerelease,
target_commitish: options.target_commitish || 'HEAD'
}
});
results.push({ repo: item.id, ok: true, message: `Tag erstellt: ${tag}` });
} else if (action === 'create-release') {
const tag = String(options.tag || '').trim();
if (!tag) throw new Error('tag-missing');
const name = String(options.name || tag).trim();
await createGiteaRelease({
token,
url: credentials.giteaURL,
owner: item.owner,
repo: item.repo,
data: {
tag_name: tag,
name,
body: options.body || '',
draft: !!options.draft,
prerelease: !!options.prerelease,
target_commitish: options.target_commitish || 'HEAD'
}
});
results.push({ repo: item.id, ok: true, message: `Release erstellt: ${name}` });
} else {
throw new Error(`unknown-action: ${action}`);
}
sendProgress({ ...progressBase, status: 'ok' });
} catch (e) {
const msg = mapIpcError(e);
results.push({ repo: item.id, ok: false, error: msg });
sendProgress({ ...progressBase, status: 'error', error: msg });
}
}
const success = results.filter(r => r.ok).length;
const failed = results.length - success;
return {
ok: true,
action,
summary: { total: results.length, success, failed },
results
};
} catch (e) {
console.error('run-batch-repo-action error', e);
return { ok: false, error: String(e) };
}
});
/* ================================ /* ================================
RENAME / CREATE / MOVE HANDLERS RENAME / CREATE / MOVE HANDLERS
================================ */ ================================ */
@@ -2025,4 +2387,24 @@ ipcMain.handle('start-update-download', async (event, asset) => {
console.error('[Main] Download-Fehler:', error); console.error('[Main] Download-Fehler:', error);
return { ok: false, error: String(error) }; return { ok: false, error: String(error) };
} }
});
ipcMain.handle('test-gitea-connection', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!url) return { ok: false, error: 'Gitea URL fehlt.' };
const result = await checkGiteaConnection({
token,
url,
timeout: (data && data.timeout) || 8000
});
return { ok: result.ok, result };
} catch (e) {
console.error('test-gitea-connection error', e);
return { ok: false, error: mapIpcError(e) };
}
}); });