Upload via Git Manager GUI - main.js

This commit is contained in:
2026-04-01 19:30:30 +00:00
parent 464d15464a
commit d6968a4954

304
main.js
View File

@@ -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();
// OPTIMIERUNG: Tray wird verzögert hergestellt (nicht beim Fenster-Create)
setImmediate(() => {
createTray(win); 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();
// OPTIMIERUNG: RetryQueue asynchron laden (nicht blockierend)
setImmediate(() => {
retryQueue = readRetryQueueFromDisk();
broadcastRetryQueueUpdate({ event: 'startup' }); broadcastRetryQueueUpdate({ event: 'startup' });
retryQueueTimer = setInterval(() => { retryQueueTimer = setInterval(() => {
processRetryQueueOnce().catch(e => console.error('processRetryQueueOnce timer error', e)); processRetryQueueOnce().catch(e => console.error('processRetryQueueOnce timer error', e));
}, RETRY_QUEUE_INTERVAL_MS); }, 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 localFiles = Array.isArray(data.localPath) ? data.localPath : (data.localPath ? [data.localPath] : []);
const results = [];
for (const localFile of localFiles) {
if (!fs.existsSync(localFile)) { results.push({ file: localFile, ok: false, error: 'local-file-not-found' }); continue; }
const raw = fs.readFileSync(localFile);
const base64 = raw.toString('base64');
const fileName = ppath.basename(localFile);
const targetPath = destPath ? `${destPath}/${fileName}` : fileName;
try {
const uploaded = await uploadGithubFile({ token: githubToken, owner, repo, path: targetPath, contentBase64: base64, message: `${message} - ${fileName}`, branch });
results.push({ file: localFile, ok: true, uploaded });
} catch (e) {
results.push({ file: localFile, ok: false, error: String(e) });
}
}
const failedCount = results.filter(r => !r.ok).length;
console.log('[UPLOAD_DEBUG][main] upload-gitea-file:github-done', { uploadDebugId, failedCount, total: results.length });
return { ok: failedCount === 0, results, failedCount, debugId: uploadDebugId };
}
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !url) return { ok: false, error: `missing-token-or-url (${uploadDebugId})` };
const owner2 = owner;
// destPath is the target folder in the repo
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
// Branch wird unverändert übernommen (main UND master werden unterstützt)
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) {
if (!fs.existsSync(localFile)) {
results.push({ file: localFile, ok: false, error: 'local-file-not-found' });
continue;
}
const raw = fs.readFileSync(localFile);
const base64 = raw.toString('base64');
const fileName = ppath.basename(localFile);
const targetPath = destPath ? `${destPath}/${fileName}` : fileName;
try {
const uploaded = await uploadGithubFile({
token: githubToken,
owner,
repo,
path: targetPath,
contentBase64: base64,
message: `${message} - ${fileName}`,
branch
});
results.push({ file: localFile, ok: true, uploaded });
logger.debug('upload-gitea-file', `File uploaded: ${fileName}`);
} catch (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;
logger.info('upload-gitea-file', `GitHub upload done`, { failedCount, total: results.length, uploadDebugId });
return { ok: failedCount === 0, results, failedCount, debugId: uploadDebugId };
}
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
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 destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
let branch = normalizeBranch(data.branch || 'HEAD', 'gitea');
const message = data.message || 'Upload via Git Manager GUI';
const localFiles = Array.isArray(data.localPath) ? data.localPath : (data.localPath ? [data.localPath] : []);
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('/')}`,