Upload main.js via GUI
This commit is contained in:
392
main.js
392
main.js
@@ -13,6 +13,7 @@ let updater = null;
|
||||
const {
|
||||
createRepoGitHub,
|
||||
createRepoGitea,
|
||||
checkGiteaConnection,
|
||||
listGiteaRepos,
|
||||
getGiteaRepoContents,
|
||||
getGiteaFileContent,
|
||||
@@ -47,6 +48,162 @@ const IV = Buffer.alloc(16, 0);
|
||||
const DEFAULT_CONCURRENCY = 4;
|
||||
// temp drag cleanup delay (ms)
|
||||
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
|
||||
@@ -115,10 +272,21 @@ function createWindow() {
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
retryQueue = readRetryQueueFromDisk();
|
||||
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('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
|
||||
app.on('before-quit', () => {
|
||||
if (retryQueueTimer) {
|
||||
clearInterval(retryQueueTimer);
|
||||
retryQueueTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
/* -----------------------------
|
||||
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)
|
||||
----------------------------- */
|
||||
@@ -244,7 +436,7 @@ ipcMain.handle('create-repo', async (event, data) => {
|
||||
} else return { ok: false, error: 'unknown-platform' };
|
||||
} catch (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 };
|
||||
} catch (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 };
|
||||
} catch (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 };
|
||||
} catch (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 };
|
||||
} catch (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 };
|
||||
} catch (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) };
|
||||
}
|
||||
});
|
||||
@@ -1397,6 +1630,135 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
||||
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
|
||||
================================ */
|
||||
@@ -2026,3 +2388,23 @@ ipcMain.handle('start-update-download', async (event, asset) => {
|
||||
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) };
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user