Files
Git-Manager-Gui/src/features/featuresIpc.js
T
2026-10-22 09:13:21 +00:00

540 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// src/features/featuresIpc.js
// IPC-Handler für: Upload-Vorschau mit Geheimnis-Warnung, Server-Diagnose,
// lokale Änderungen (Status/Diff/Commit), Pull Requests & Issues und Repo-Backup.
const fs = require('fs');
const os = require('os');
const path = require('path');
const dns = require('dns').promises;
const net = require('net');
const http = require('http');
const https = require('https');
const { spawn } = require('child_process');
const axios = require('axios');
const { normalizeAndValidateBaseUrl, uploadItemsViaGiteaApi } = require('../git/apiHandler.js');
const { scanForSecrets } = require('./secretScan.js');
const { createRepoBackup, authHeaderArgs } = require('../backup/GiteaRepoBackup.js');
const { commitIdentity } = require('../git/gitBinary.js');
const { createRepoRestore } = require('../backup/GiteaRepoRestore.js');
const agentOptions = { keepAlive: true, autoSelectFamily: true, autoSelectFamilyAttemptTimeout: 250 };
const api = axios.create({
httpAgent: new http.Agent(agentOptions),
httpsAgent: new https.Agent(agentOptions),
timeout: 30000,
validateStatus: () => true
});
const enc = encodeURIComponent;
const repoApiPath = (owner, repo) => `/repos/${enc(owner)}/${enc(repo)}`;
function giteaContext(readCredentials) {
const c = readCredentials() || {};
if (!c.giteaURL || !c.giteaToken) throw new Error('Gitea-Zugangsdaten fehlen. Bitte in den Einstellungen eintragen.');
const base = normalizeAndValidateBaseUrl(c.giteaURL);
return { base, apiBase: `${base}/api/v1`, token: c.giteaToken, headers: { Authorization: `token ${c.giteaToken}` } };
}
async function giteaCall(ctx, method, apiPath, { params, data, timeout } = {}) {
const res = await api.request({ method, url: ctx.apiBase + apiPath, params, data, headers: ctx.headers, timeout });
if (res.status >= 200 && res.status < 300) return res.data;
const err = new Error((res.data && res.data.message) || `HTTP ${res.status}`);
err.status = res.status;
throw err;
}
// Git mit Zeitlimit, ohne den Hauptprozess zu blockieren.
function runGit(args, { cwd, timeoutMs = 60000 } = {}) {
return new Promise((resolve) => {
let child;
try {
child = spawn('git', args, { cwd, windowsHide: true, env: process.env });
} catch (e) {
resolve({ status: -1, stdout: '', stderr: String(e && e.message || e) });
return;
}
const out = [];
const err = [];
let timedOut = false;
const timer = setTimeout(() => { timedOut = true; try { child.kill(); } catch (_) {} }, timeoutMs);
child.stdout.on('data', c => out.push(c));
child.stderr.on('data', c => err.push(c));
child.on('error', e => err.push(Buffer.from(String(e && e.message || e))));
child.on('close', (code) => {
clearTimeout(timer);
resolve({
status: timedOut ? -2 : code,
timedOut,
stdout: Buffer.concat(out).toString('utf8'),
stderr: Buffer.concat(err).toString('utf8')
});
});
child.stdin.end();
});
}
// Token nie in Fehlermeldungen oder Logs durchreichen.
function redact(text, token) {
let s = String(text || '');
if (token) s = s.split(token).join('***');
return s.replace(/\/\/[^/@\s]+@/g, '//***@');
}
function isPackFailure(stderr) {
return /early eof|index-pack|bad pack header|unexpected disconnect|remote end hung up|rpc failed/i.test(String(stderr || ''));
}
/* ----------------------------------------------------------------
Upload-Vorschau
---------------------------------------------------------------- */
async function previewUpload({ paths, destPath }, deps) {
const { items, skipped, ignored, blocked } = await deps.buildItemsFromPaths(paths, destPath);
const files = [];
let totalBytes = 0;
for (const item of items) {
let size = 0;
try { size = (await fs.promises.stat(item.localFile)).size; } catch (_) {}
totalBytes += size;
files.push({ localFile: item.localFile, targetPath: item.targetPath, size });
}
const prefs = deps.prefs ? deps.prefs.get() : { uploadSecretScan: true, uploadPreview: 'always' };
// Geheimnis-Erkennung lässt sich unter Einstellungen → Uploads abschalten
const secrets = prefs.uploadSecretScan ? await scanForSecrets(files) : new Map();
for (const f of files) {
const reason = secrets.get(f.localFile);
if (reason) f.secret = reason;
}
const ignoredEntries = [
...skipped.filter(s => s.error === 'gitignore').map(s => ({ path: s.path, isDir: isDirSafe(s.path) })),
...(ignored || [])
];
return {
ok: true, files, totalBytes, ignored: ignoredEntries, secretCount: secrets.size, blocked: blocked || [],
previewMode: prefs.uploadPreview, secretScan: prefs.uploadSecretScan
};
}
function isDirSafe(p) {
try { return fs.statSync(p).isDirectory(); } catch (_) { return false; }
}
/* ----------------------------------------------------------------
Server-Diagnose
---------------------------------------------------------------- */
const SERVER_HINT_COMMANDS = 'docker logs --tail 300 gitea 2>&1 | grep -iE "pack|upload-pack|error|killed"\ndf -h\nfree -m';
async function probeFamily(ctx, family) {
const lib = ctx.base.startsWith('https:') ? https : http;
const agent = new lib.Agent({ family, keepAlive: false });
const started = Date.now();
try {
const res = await axios.get(`${ctx.apiBase}/version`, {
httpAgent: agent, httpsAgent: agent, timeout: 8000, validateStatus: () => true
});
return { ok: res.status < 500, ms: Date.now() - started, status: res.status };
} catch (e) {
return { ok: false, error: e.code || e.message };
} finally {
agent.destroy();
}
}
async function runDiagnosis(data, deps) {
const checks = [];
const add = (label, status, detail, hint = null) => checks.push({ label, status, detail, hint });
let ctx;
try {
ctx = giteaContext(deps.readCredentials);
add('Zugangsdaten', 'ok', `Server: ${ctx.base}`);
} catch (e) {
add('Zugangsdaten', 'fail', e.message, 'In den Einstellungen Gitea-URL und Token eintragen.');
return { ok: true, checks };
}
// Namensauflösung und Erreichbarkeit pro Adressfamilie
const host = new URL(ctx.base).hostname.replace(/^\[|\]$/g, '');
let families = [];
if (net.isIP(host)) {
families = [net.isIP(host)];
add('Namensauflösung', 'ok', `IP-Adresse direkt angegeben (IPv${families[0]})`);
} else {
try {
const addrs = await dns.lookup(host, { all: true });
const v4 = addrs.filter(a => a.family === 4).map(a => a.address);
const v6 = addrs.filter(a => a.family === 6).map(a => a.address);
if (v4.length) families.push(4);
if (v6.length) families.push(6);
add('Namensauflösung', 'ok', [v4.length ? `IPv4: ${v4.join(', ')}` : null, v6.length ? `IPv6: ${v6.join(', ')}` : null].filter(Boolean).join(' · '));
} catch (e) {
add('Namensauflösung', 'fail', `${host} lässt sich nicht auflösen (${e.code || e.message})`,
'DNS-Eintrag (bei ipv64.net: DynDNS-Update) prüfen und ob der Name richtig geschrieben ist.');
return { ok: true, checks };
}
}
const reach = {};
for (const fam of families) reach[fam] = await probeFamily(ctx, fam);
const working = families.filter(f => reach[f].ok);
for (const fam of families) {
const r = reach[fam];
if (r.ok) add(`Erreichbar über IPv${fam}`, 'ok', `Antwort in ${r.ms} ms`);
else add(`Erreichbar über IPv${fam}`, working.length ? 'warn' : 'fail', `Keine Verbindung (${r.error || 'HTTP ' + r.status})`,
working.length
? `Die App weicht automatisch auf IPv${working[0]} aus. Falls IPv${fam} gewollt ist: Firewall/Portfreigabe und ${fam === 6 ? 'AAAA' : 'A'}-Eintrag prüfen.`
: 'Server läuft nicht oder Port/Firewall/Reverse-Proxy blockiert.');
}
if (!working.length) return { ok: true, checks };
// API und Token
let login = null;
try {
const v = await giteaCall(ctx, 'get', '/version');
add('Gitea-API', 'ok', `Gitea ${v && v.version ? v.version : '(Version unbekannt)'}`);
} catch (e) {
add('Gitea-API', 'fail', e.message, 'Gitea-URL prüfen, sie muss auf die Startseite von Gitea zeigen (ohne /api).');
return { ok: true, checks };
}
try {
const u = await giteaCall(ctx, 'get', '/user');
login = u.login;
add('Token', 'ok', `Angemeldet als ${u.login}`);
} catch (e) {
add('Token', 'fail', e.status === 401 ? 'Token ungültig oder abgelaufen' : e.message, 'In Gitea unter Einstellungen → Anwendungen einen neuen Token erzeugen.');
return { ok: true, checks };
}
// Git-Transport mit einem echten Repo testen
let owner = data && data.owner;
let repo = data && data.repo;
if (!owner || !repo) {
try {
const repos = await giteaCall(ctx, 'get', '/user/repos', { params: { limit: 1 } });
if (repos && repos[0]) { owner = repos[0].owner.login; repo = repos[0].name; }
} catch (_) {}
}
if (!owner || !repo) {
add('Git-Transport', 'warn', 'Kein Repository zum Testen gefunden');
} else {
const u = new URL(ctx.base);
const authUrl = `${u.protocol}//${enc(ctx.token)}@${u.host}${u.pathname.replace(/\/$/, '')}/${owner}/${repo}.git`;
const lr = await runGit(['ls-remote', '--heads', authUrl], { timeoutMs: 30000 });
if (lr.status !== 0) {
add('Git-Anmeldung', 'fail', `ls-remote für ${owner}/${repo} fehlgeschlagen: ${redact(lr.stderr, ctx.token).trim().slice(0, 300)}`,
'Token braucht Lese- und Schreibrechte für Repositories. Reverse-Proxy muss /info/refs durchlassen.');
} else {
add('Git-Anmeldung', 'ok', `${owner}/${repo} erreichbar`);
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-diag-'));
try {
const started = Date.now();
let pk = await runGit(['clone', '--bare', '--depth', '1', '--filter=tree:0', authUrl, path.join(tmp, 'r.git')], { timeoutMs: 90000 });
if (pk.status !== 0 && /filter|not recognized|unsupported/i.test(pk.stderr) && !isPackFailure(pk.stderr)) {
pk = await runGit(['clone', '--bare', '--depth', '1', authUrl, path.join(tmp, 'r2.git')], { timeoutMs: 90000 });
}
if (pk.status === 0) {
add('Git-Pakete', 'ok', `Server liefert Pakete aus (${Date.now() - started} ms)`);
if (deps.clearGitTransportBlock && deps.clearGitTransportBlock(u.host)) {
add('Schneller Git-Weg', 'ok', 'Sperre aufgehoben: Uploads und Löschungen laufen wieder über Git statt über die langsame API.');
}
} else if (pk.timedOut) {
add('Git-Pakete', 'fail', 'Zeitüberschreitung beim Abholen eines Pakets', 'Verbindung oder Server sehr langsam. Reverse-Proxy-Timeouts prüfen.');
} else if (isPackFailure(pk.stderr)) {
add('Git-Pakete', 'fail',
'Der Server bricht beim Ausliefern von Git-Paketen ab (early EOF). Deshalb laufen Uploads und Löschungen über die langsame API.',
'Das liegt am Server. Häufige Ursachen: Datenträger voll, zu wenig RAM für git pack-objects, defektes Repo oder ein Proxy, der die Antwort abschneidet. Auf dem Server prüfen:\n' + SERVER_HINT_COMMANDS);
} else {
add('Git-Pakete', 'fail', redact(pk.stderr, ctx.token).trim().slice(0, 300));
}
} finally {
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_) {}
}
}
}
add('Speicherplatz & RAM', 'info', 'Lassen sich über die Gitea-API nicht abfragen.', 'Auf dem Server prüfen mit:\ndf -h\nfree -m');
return { ok: true, checks, login };
}
/* ----------------------------------------------------------------
Lokale Änderungen
---------------------------------------------------------------- */
const STATUS_LABELS = { M: 'geändert', A: 'neu', D: 'gelöscht', R: 'umbenannt', C: 'kopiert', U: 'Konflikt', '?': 'neu (nicht verfolgt)', T: 'Typ geändert' };
function parsePorcelainZ(out) {
const parts = out.split('\0');
const files = [];
for (let i = 0; i < parts.length; i++) {
const entry = parts[i];
if (!entry) continue;
const x = entry[0];
const y = entry[1];
const file = entry.slice(3);
let from = null;
if (x === 'R' || x === 'C') { from = parts[i + 1]; i++; }
const code = x === '?' ? '?' : (x === 'U' || y === 'U' || (x === 'A' && y === 'A') || (x === 'D' && y === 'D')) ? 'U' : (y !== ' ' ? y : x);
files.push({ path: file, from, code, label: STATUS_LABELS[code] || code, staged: x !== ' ' && x !== '?' });
}
return files;
}
async function localGit(data, deps = {}) {
const folder = data && data.folder;
if (!folder || !fs.existsSync(folder)) return { ok: false, error: 'Ordner nicht gefunden' };
const top = await runGit(['-C', folder, 'rev-parse', '--show-toplevel']);
if (top.status !== 0) return { ok: false, notRepo: true, error: 'Dieser Ordner ist kein Git-Repository.' };
const root = path.resolve(top.stdout.trim());
switch (data.action) {
case 'status': {
const st = await runGit(['-C', root, 'status', '--porcelain=v1', '-z', '-uall']);
if (st.status !== 0) return { ok: false, error: st.stderr.trim() };
const br = await runGit(['-C', root, 'rev-parse', '--abbrev-ref', 'HEAD']);
const ahead = await runGit(['-C', root, 'rev-list', '--count', '@{u}..HEAD']);
return {
ok: true, root,
branch: br.status === 0 ? br.stdout.trim() : '(noch kein Commit)',
ahead: ahead.status === 0 ? Number(ahead.stdout.trim()) || 0 : null,
files: parsePorcelainZ(st.stdout)
};
}
case 'diff': {
const file = String(data.file || '');
if (!file) return { ok: false, error: 'Keine Datei' };
const abs = path.join(root, file);
if (data.code === '?' || data.code === 'A') {
try {
const st = await fs.promises.stat(abs);
if (st.size > 300 * 1024) return { ok: true, diff: `Neue Datei (${Math.round(st.size / 1024)} KB), zu groß für die Vorschau.` };
const buf = await fs.promises.readFile(abs);
if (buf.subarray(0, 8000).includes(0)) return { ok: true, diff: 'Neue Binärdatei' };
return { ok: true, diff: buf.toString('utf8').split('\n').map(l => '+' + l).join('\n'), allNew: true };
} catch (e) {
return { ok: false, error: e.message };
}
}
const d = await runGit(['-C', root, 'diff', 'HEAD', '--no-color', '--', file]);
if (d.status !== 0) return { ok: false, error: d.stderr.trim() };
return { ok: true, diff: d.stdout.length > 400000 ? d.stdout.slice(0, 400000) + '\n… (gekürzt)' : (d.stdout || 'Keine inhaltlichen Unterschiede (z. B. nur Zeilenenden).') };
}
case 'commit': {
const files = Array.isArray(data.files) ? data.files.filter(Boolean) : [];
const message = String(data.message || '').trim();
if (!files.length) return { ok: false, error: 'Keine Dateien ausgewählt' };
if (!message) return { ok: false, error: 'Commit-Nachricht fehlt' };
// Nur die ausgewählten Dateien in den Commit nehmen.
await runGit(['-C', root, 'reset', '-q']);
for (let i = 0; i < files.length; i += 100) {
const r = await runGit(['-C', root, 'add', '-A', '--', ...files.slice(i, i + 100)]);
if (r.status !== 0) return { ok: false, error: r.stderr.trim() };
}
let c = await runGit(['-C', root, 'commit', '-m', message]);
if (c.status !== 0 && /tell me who you are|user\.email|user\.name/i.test(c.stderr)) {
const id = commitIdentity(deps.prefs && deps.prefs.get());
c = await runGit(['-C', root, '-c', `user.name=${id.name}`, '-c', `user.email=${id.email}`, 'commit', '-m', message]);
}
if (c.status !== 0) return { ok: false, error: (c.stderr || c.stdout).trim() };
return { ok: true, output: c.stdout.trim() };
}
case 'push': {
const p = await runGit(['-C', root, 'push', 'origin', 'HEAD'], { timeoutMs: 600000 });
if (p.status !== 0) {
return { ok: false, error: redact(p.stderr || p.stdout, null).trim() || 'Push fehlgeschlagen',
hint: isPackFailure(p.stderr) ? 'Der Server bricht die Übertragung ab. Server-Diagnose in den Einstellungen ausführen.' : null };
}
return { ok: true, output: p.stderr.trim() };
}
case 'discard': {
const file = String(data.file || '');
if (!file) return { ok: false, error: 'Keine Datei' };
if (data.code === '?') {
await fs.promises.rm(path.join(root, file), { force: true });
return { ok: true };
}
const r = await runGit(['-C', root, 'checkout', 'HEAD', '--', file]);
return r.status === 0 ? { ok: true } : { ok: false, error: r.stderr.trim() };
}
default:
return { ok: false, error: 'Unbekannte Aktion' };
}
}
/* ----------------------------------------------------------------
Pull Requests & Issues
---------------------------------------------------------------- */
function mapUser(u) { return u ? { login: u.login, avatar: u.avatar_url } : null; }
function mapIssue(i) {
return {
number: i.number, title: i.title, body: i.body || '', state: i.state,
user: mapUser(i.user), comments: i.comments, created: i.created_at, updated: i.updated_at,
labels: (i.labels || []).map(l => ({ name: l.name, color: l.color })), url: i.html_url,
isPull: !!i.pull_request
};
}
function mapPull(p) {
return {
...mapIssue(p), isPull: true,
head: p.head && p.head.label, base: p.base && p.base.label,
mergeable: p.mergeable, merged: !!p.merged
};
}
async function giteaCollab(data, deps) {
const ctx = giteaContext(deps.readCredentials);
const { owner, repo } = data || {};
if (!owner || !repo) return { ok: false, error: 'Repository fehlt' };
const rp = repoApiPath(owner, repo);
const idx = Number(data.index);
const state = ['open', 'closed', 'all'].includes(data.state) ? data.state : 'open';
const page = Math.max(1, Number(data.page) || 1);
switch (data.action) {
case 'listPulls': {
const list = await giteaCall(ctx, 'get', `${rp}/pulls`, { params: { state, page, limit: 30, sort: 'recentupdate' } });
return { ok: true, items: (list || []).map(mapPull) };
}
case 'listIssues': {
const list = await giteaCall(ctx, 'get', `${rp}/issues`, { params: { state, page, limit: 30, type: 'issues' } });
return { ok: true, items: (list || []).map(mapIssue) };
}
case 'get': {
const item = data.isPull
? mapPull(await giteaCall(ctx, 'get', `${rp}/pulls/${idx}`))
: mapIssue(await giteaCall(ctx, 'get', `${rp}/issues/${idx}`));
const comments = await giteaCall(ctx, 'get', `${rp}/issues/${idx}/comments`);
return {
ok: true, item,
comments: (comments || []).map(c => ({ id: c.id, body: c.body || '', user: mapUser(c.user), created: c.created_at }))
};
}
case 'comment': {
const body = String(data.body || '').trim();
if (!body) return { ok: false, error: 'Kommentar ist leer' };
await giteaCall(ctx, 'post', `${rp}/issues/${idx}/comments`, { data: { body } });
return { ok: true };
}
case 'setState': {
if (!['open', 'closed'].includes(data.state)) return { ok: false, error: 'Ungültiger Status' };
await giteaCall(ctx, 'patch', `${rp}/issues/${idx}`, { data: { state: data.state } });
return { ok: true };
}
case 'merge': {
const style = ['merge', 'rebase', 'rebase-merge', 'squash'].includes(data.style) ? data.style : 'merge';
await giteaCall(ctx, 'post', `${rp}/pulls/${idx}/merge`, {
data: { Do: style, delete_branch_after_merge: !!data.deleteBranch },
timeout: 120000
});
return { ok: true };
}
case 'createIssue': {
const title = String(data.title || '').trim();
if (!title) return { ok: false, error: 'Titel fehlt' };
const created = await giteaCall(ctx, 'post', `${rp}/issues`, { data: { title, body: String(data.body || '') } });
return { ok: true, item: mapIssue(created) };
}
default:
return { ok: false, error: 'Unbekannte Aktion' };
}
}
/* ----------------------------------------------------------------
Registrierung
---------------------------------------------------------------- */
function wrap(fn) {
return async (event, data) => {
try {
return await fn(data || {}, event);
} catch (e) {
console.error('[features]', e && e.message ? e.message : e);
return { ok: false, error: e && e.message ? e.message : String(e) };
}
};
}
function registerFeatureIpc(deps) {
const { ipcMain } = deps;
ipcMain.handle('preview-upload-paths', wrap((data) => previewUpload(data, deps)));
ipcMain.handle('run-server-diagnosis', wrap((data) => runDiagnosis(data, deps)));
ipcMain.handle('local-git', wrap((data) => localGit(data, deps)));
// App-Einstellungen (ohne Geheimnisse)
ipcMain.handle('prefs-get', wrap(() => ({ ok: true, prefs: deps.prefs ? deps.prefs.get() : {} })));
ipcMain.handle('prefs-set', wrap((data) => {
if (!deps.prefs) return { ok: false, error: 'Einstellungen nicht verfügbar' };
const r = deps.prefs.set(data);
return { ok: r.rejected.length === 0, prefs: r.prefs, rejected: r.rejected,
error: r.rejected.includes('gitEmail') ? 'Die E-Mail-Adresse ist ungültig und wurde nicht übernommen.' : undefined };
}));
// Gesperrter schneller Git-Weg (nach "early EOF") anzeigen und von Hand freigeben
ipcMain.handle('git-transport-state', wrap(() => {
const blocks = deps.getGitTransportBlocks ? deps.getGitTransportBlocks() : {};
return { ok: true, blocked: Object.entries(blocks).map(([host, since]) => ({ host, since: new Date(since).toISOString() })) };
}));
ipcMain.handle('git-transport-clear', wrap((data) => {
const host = String((data && data.host) || '');
return { ok: !!(deps.clearGitTransportBlock && deps.clearGitTransportBlock(host)) };
}));
ipcMain.handle('gitea-collab', wrap((data) => giteaCollab(data, deps)));
const backup = createRepoBackup({
configPath: path.join(deps.app.getPath('userData'), 'repo-backup.json'),
getContext: () => giteaContext(deps.readCredentials),
giteaCall,
runGit,
isPackFailure,
redact
});
ipcMain.handle('repo-backup-get-config', wrap(() => ({ ok: true, ...backup.getState() })));
ipcMain.handle('repo-backup-set-config', wrap((data) => ({ ok: true, ...backup.setConfig(data) })));
// Fortschritt an alle Fenster melden – auch bei einem geplanten Backup und für später geöffnete Ansichten
backup.onProgress((p) => {
if (deps.broadcast) deps.broadcast('repo-backup-progress', p);
// Windows-Benachrichtigung am Ende – je nach Einstellung immer, nur bei Problemen oder nie
if (!p || (p.phase !== 'done' && p.phase !== 'error') || !deps.notify) return;
const mode = backup.getState().config.notify;
const s = p.summary;
const problem = p.phase === 'error' || (s && (s.failed > 0 || s.cancelled));
if (mode === 'never' || (mode === 'errors' && !problem)) return;
if (p.phase === 'error') deps.notify('Repo-Backup fehlgeschlagen', p.error || 'Unbekannter Fehler');
else if (s.cancelled) deps.notify('Repo-Backup abgebrochen', `${s.ok} von ${s.total} Repos gesichert.`);
else if (s.failed) deps.notify('Repo-Backup mit Fehlern', `${s.ok} von ${s.total} gesichert, ${s.failed} fehlgeschlagen. Details in den Einstellungen unter Backup.`);
else deps.notify('Repo-Backup fertig', `${s.ok} von ${s.total} Repos gesichert.`);
});
// Ordner im Explorer öffnen – nur feste, bekannte Ziele, keine beliebigen Pfade aus dem Renderer
ipcMain.handle('open-known-folder', wrap(async (data) => {
const targets = {
backup: () => backup.getState().config.targetDir,
appData: () => deps.app.getPath('userData'),
logs: () => deps.app.getPath('logs')
};
const resolve = targets[data && data.which];
const dir = resolve && resolve();
if (!dir) return { ok: false, error: 'Kein Ordner festgelegt.' };
if (!fs.existsSync(dir)) return { ok: false, error: `Ordner "${dir}" nicht gefunden.` };
const err = deps.openPath ? await deps.openPath(dir) : 'Öffnen nicht möglich';
return err ? { ok: false, error: err } : { ok: true, path: dir };
}));
ipcMain.handle('repo-backup-run-now', wrap(() => backup.runNow(null)));
ipcMain.handle('repo-backup-cancel', wrap(() => backup.cancel()));
backup.startScheduler();
const restore = createRepoRestore({
getContext: () => giteaContext(deps.readCredentials),
giteaCall, runGit, isPackFailure, redact, uploadItemsViaGiteaApi, authHeaderArgs,
identity: () => commitIdentity(deps.prefs && deps.prefs.get())
});
ipcMain.handle('repo-backup-list', wrap((data) => {
const dir = (data && data.dir) || backup.getState().config.targetDir;
if (!dir) return { ok: false, error: 'Kein Backup-Ordner festgelegt.' };
if (!fs.existsSync(dir)) return { ok: false, error: `Backup-Ordner "${dir}" nicht gefunden.` };
return { ok: true, dir, backups: restore.listBackups(dir) };
}));
ipcMain.handle('repo-restore-local', wrap((data) => restore.restoreLocal(data)));
ipcMain.handle('repo-restore-gitea', wrap((data, event) => restore.restoreToGitea(data, (p) => {
try { event.sender.send('repo-restore-progress', p); } catch (_) {}
})));
return { backup, restore };
}
module.exports = { registerFeatureIpc, parsePorcelainZ };