Upload file main.js via GUI

This commit is contained in:
2026-03-01 10:31:11 +01:00
parent 43750e2d8a
commit 223714d5e3

463
main.js
View File

@@ -252,19 +252,13 @@ ipcMain.handle('push-project', async (event, data) => {
try {
if (!data.folder || !fs.existsSync(data.folder)) return { ok: false, error: 'folder-not-found' };
// Prüfen, ob der lokale Branch 'master' heißt und in 'main' umbenennen
// Aktuellen Branch ermitteln (NICHT umbenennen!)
let currentBranch = data.branch || null;
try {
const currentBranch = execSync('git branch --show-current', { cwd: data.folder, stdio: 'pipe', encoding: 'utf-8' }).trim();
console.log('Current local branch:', currentBranch);
if (currentBranch === 'master') {
console.log('Attempting to rename master to main...');
try {
execSync('git branch -m master main', { cwd: data.folder, stdio: 'inherit' });
console.log('Successfully renamed local branch master to main');
} catch (e) {
console.warn('Failed to rename branch (maybe main already exists)', e.message);
}
const detected = execSync('git branch --show-current', { cwd: data.folder, stdio: 'pipe', encoding: 'utf-8' }).trim();
if (detected) {
currentBranch = currentBranch || detected;
console.log('Current local branch:', detected);
}
} catch (e) {
console.warn('Could not check local branch (maybe not a git repo yet)', e.message);
@@ -310,9 +304,11 @@ ipcMain.handle('push-project', async (event, data) => {
}
}
// 3. Pushen (nutze 'main')
// 3. Pushen (nutze den tatsächlichen Branch - main ODER master)
const progressCb = percent => { try { event.sender.send('push-progress', percent); } catch (_) {} };
await commitAndPush(data.folder, 'main', 'Update from Git Manager GUI', progressCb);
const commitMsg = data.commitMessage || 'Update from Git Manager GUI';
const pushBranch = currentBranch || 'main';
await commitAndPush(data.folder, pushBranch, commitMsg, progressCb);
return { ok: true };
} catch (e) {
console.error('push-project error', e);
@@ -323,8 +319,20 @@ ipcMain.handle('push-project', async (event, data) => {
ipcMain.handle('getBranches', async (event, data) => {
try {
const branches = await getBranches(data.folder);
// Sortieren, damit 'main' oben steht
branches.sort((a, b) => (a === 'main' ? -1 : b === 'main' ?1 : 0));
// Aktuellen Branch ermitteln und nach oben sortieren
let currentBranch = null;
try {
currentBranch = execSync('git branch --show-current', { cwd: data.folder, stdio: 'pipe', encoding: 'utf-8' }).trim();
} catch (_) {}
branches.sort((a, b) => {
if (a === currentBranch) return -1;
if (b === currentBranch) return 1;
if (a === 'main') return -1;
if (b === 'main') return 1;
if (a === 'master') return -1;
if (b === 'master') return 1;
return 0;
});
return { ok: true, branches };
} catch (e) {
console.error('getBranches error', e);
@@ -463,6 +471,121 @@ ipcMain.handle('writeFile', async (event, data) => {
ipcMain.handle('deleteFile', async (event, data) => {
try {
// --- GITEA DELETION ---
if (data && data.isGitea) {
const credentials = readCredentials();
const token = (data.token) || (credentials && credentials.giteaToken);
const giteaUrl = (data.url) || (credentials && credentials.giteaURL);
if (!token || !giteaUrl) return { ok: false, error: 'missing-token-or-url' };
const owner = data.owner;
const repo = data.repo;
const filePath = data.path;
if (!owner || !repo || !filePath) return { ok: false, error: 'missing-owner-repo-or-path' };
const urlObj = new URL(giteaUrl);
const protocol = urlObj.protocol === 'https:' ? https : http;
const port = urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80);
// Helper: GET contents from Gitea API
function giteaGet(path) {
return new Promise((resolve, reject) => {
const req = protocol.request({
hostname: urlObj.hostname, port,
path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${path.split('/').map(encodeURIComponent).join('/')}?ref=${data.ref || 'HEAD'}`,
method: 'GET',
headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' }
}, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
try { resolve(JSON.parse(body)); } catch (e) { reject(e); }
});
});
req.on('error', reject);
req.end();
});
}
// Helper: DELETE a single file by path + sha
function giteaDeleteFile(filePath, sha) {
return new Promise((resolve) => {
const body = JSON.stringify({
message: `Delete ${filePath} via Git Manager GUI`,
sha,
branch: data.ref || 'HEAD'
});
const req = protocol.request({
hostname: urlObj.hostname, port,
path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${filePath.split('/').map(encodeURIComponent).join('/')}`,
method: 'DELETE',
headers: {
'Authorization': `token ${token}`,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body)
}
}, (res) => {
let respBody = '';
res.on('data', chunk => respBody += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) resolve({ ok: true });
else resolve({ ok: false, error: `HTTP ${res.statusCode}: ${respBody}` });
});
});
req.on('error', (e) => resolve({ ok: false, error: String(e) }));
req.write(body);
req.end();
});
}
// Helper: Recursively collect all files in a folder
async function collectAllFiles(path) {
const contents = await giteaGet(path);
const files = [];
if (Array.isArray(contents)) {
// It's a folder — recurse into it
for (const item of contents) {
if (item.type === 'dir') {
const sub = await collectAllFiles(item.path);
files.push(...sub);
} else if (item.type === 'file') {
files.push({ path: item.path, sha: item.sha });
}
}
} else if (contents && contents.sha) {
// It's a single file
files.push({ path: contents.path, sha: contents.sha });
} else {
throw new Error(`Unbekannte Antwort: ${JSON.stringify(contents)}`);
}
return files;
}
// Collect all files to delete (handles both file and folder)
const filesToDelete = await collectAllFiles(filePath);
if (filesToDelete.length === 0) {
return { ok: false, error: 'Keine Dateien zum Löschen gefunden' };
}
// Delete all files sequentially
let failed = 0;
for (const f of filesToDelete) {
const res = await giteaDeleteFile(f.path, f.sha);
if (!res.ok) {
console.error(`Fehler beim Löschen von ${f.path}:`, res.error);
failed++;
}
}
if (failed > 0) {
return { ok: false, error: `${failed} von ${filesToDelete.length} Dateien konnten nicht gelöscht werden` };
}
return { ok: true, deleted: filesToDelete.length };
}
// --- LOCAL DELETION ---
if (!data || !data.path || !fs.existsSync(data.path)) return { ok: false, error: 'file-not-found' };
fs.rmSync(data.path, { recursive: true, force: true });
return { ok: true };
@@ -503,8 +626,8 @@ ipcMain.handle('get-gitea-repo-contents', async (event, data) => {
// This allows apiHandler.js to try ['main', 'master'] if no ref is passed
const ref = data.ref;
const items = await getGiteaRepoContents({ token, url, owner, repo, path: p, ref });
return { ok: true, items };
const result = await getGiteaRepoContents({ token, url, owner, repo, path: p, ref });
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) };
@@ -546,7 +669,7 @@ ipcMain.handle('read-gitea-file', async (event, data) => {
const owner = data.owner;
const repo = data.repo;
const p = data.path;
const ref = data.ref || 'main';
const ref = data.ref || 'HEAD';
console.log(`read-gitea-file: ${owner}/${repo}/${p} (ref: ${ref})`);
@@ -619,9 +742,8 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
const repo = data.repo;
// destPath is the target folder in the repo
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
// FIXED: Konvertiere 'master' zu 'main' (Upload should generally target main)
let branch = data.branch || 'main';
if (branch === 'master') branch = 'main';
// Branch wird unverändert übernommen (main UND master werden unterstützt)
let branch = data.branch || 'HEAD';
const message = data.message || 'Upload via Git Manager GUI';
const localFiles = Array.isArray(data.localPath) ? data.localPath : (data.localPath ? [data.localPath] : []);
const results = [];
@@ -678,7 +800,7 @@ ipcMain.handle('write-gitea-file', async (event, data) => {
const repo = data.repo;
const path = data.path;
const content = data.content || '';
const ref = data.ref || 'main';
const ref = data.ref || 'HEAD';
// Konvertiere Content zu Base64
const base64 = Buffer.from(content, 'utf8').toString('base64');
@@ -712,9 +834,8 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
const repo = data.repo;
// destPath is the target directory in the repo
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
// FIXED: Konvertiere 'master' zu 'main'
let branch = data.branch || 'main';
if (branch === 'master') branch = 'main';
// Branch wird unverändert übernommen (main UND master werden unterstützt)
let branch = data.branch || 'HEAD';
const messagePrefix = data.messagePrefix || 'Upload folder via GUI';
const concurrency = data.concurrency || DEFAULT_CONCURRENCY;
if (!localFolder || !fs.existsSync(localFolder)) return { ok: false, error: 'local-folder-not-found' };
@@ -792,7 +913,7 @@ ipcMain.handle('download-gitea-file', async (event, data) => {
const repo = data.repo;
const filePath = data.path;
if (!owner || !repo || !filePath) return { ok: false, error: 'missing-owner-repo-or-path' };
const content = await getGiteaFileContent({ token, url, owner, repo, path: filePath, ref: 'main' });
const content = await getGiteaFileContent({ token, url, owner, repo, path: filePath, ref: data.ref || 'HEAD' });
const save = await dialog.showSaveDialog({ defaultPath: ppath.basename(filePath) });
if (save.canceled || !save.filePath) return { ok: false, error: 'save-canceled' };
fs.writeFileSync(save.filePath, content, 'utf8');
@@ -820,7 +941,8 @@ ipcMain.handle('download-gitea-folder', async (event, data) => {
const allFiles = [];
async function gather(pathInRepo) {
const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: 'main' });
const _r = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'HEAD' });
const items = _r.items || _r;
for (const item of items) {
if (item.type === 'dir') await gather(item.path);
else if (item.type === 'file') allFiles.push(item.path);
@@ -832,7 +954,7 @@ ipcMain.handle('download-gitea-folder', async (event, data) => {
if (total === 0) return { ok: true, savedTo: destBase, files: [] };
const tasks = allFiles.map(remoteFile => async () => {
const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: 'main' });
const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: data.ref || 'HEAD' });
const localPath = ppath.join(destBase, remoteFile);
fs.mkdirSync(ppath.dirname(localPath), { recursive: true });
fs.writeFileSync(localPath, content, 'utf8');
@@ -892,8 +1014,9 @@ ipcMain.handle('prepare-download-drag', async (event, data) => {
// Gather list of files (recursive)
const allFiles = [];
async function gather(pathInRepo) {
const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'main' });
for (const item of items || []) {
const _r2 = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'HEAD' });
const items = (_r2.items || _r2) || [];
for (const item of items) {
if (item.type === 'dir') await gather(item.path);
else if (item.type === 'file') allFiles.push(item.path);
}
@@ -909,7 +1032,7 @@ ipcMain.handle('prepare-download-drag', async (event, data) => {
// Download files sequentially or with limited concurrency:
const tasks = allFiles.map(remoteFile => async () => {
const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: data.ref || 'main' });
const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: data.ref || 'HEAD' });
const localPath = ppath.join(tmpBase, remoteFile);
ensureDir(ppath.dirname(localPath));
@@ -1030,9 +1153,8 @@ ipcMain.handle('upload-and-push', async (event, data) => {
const giteaUrl = (data && data.url) || (credentials && credentials.giteaURL);
const owner = data.owner;
const repo = data.repo;
// FIXED: Konvertiere 'master' zu 'main'
let branch = data.branch || 'main';
if (branch === 'master') branch = 'main';
// Branch wird unverändert übernommen (main UND master werden unterstützt)
let branch = data.branch || 'HEAD';
const cloneUrl = data.cloneUrl || null;
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
if (!owner || !repo) return { ok: false, error: 'missing-owner-or-repo' };
@@ -1275,6 +1397,226 @@ ipcMain.handle('upload-and-push', async (event, data) => {
return { ok: false, error: String(e) };
}
});
/* ================================
RENAME / CREATE / MOVE HANDLERS
================================ */
// Gitea: Datei/Ordner umbenennen (= alle Dateien kopieren + alte löschen)
ipcMain.handle('rename-gitea-item', async (event, data) => {
try {
const credentials = readCredentials();
const token = credentials?.giteaToken;
const giteaUrl = credentials?.giteaURL;
if (!token || !giteaUrl) return { ok: false, error: 'missing-token-or-url' };
const { owner, repo, oldPath, newPath, isDir } = data;
const urlObj = new URL(giteaUrl);
const protocol = urlObj.protocol === 'https:' ? https : http;
const port = urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80);
function giteaRequest(method, apiPath, body) {
return new Promise((resolve, reject) => {
const bodyStr = body ? JSON.stringify(body) : null;
const req = protocol.request({
hostname: urlObj.hostname, port,
path: apiPath, method,
headers: {
'Authorization': `token ${token}`,
'Content-Type': 'application/json',
...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})
}
}, (res) => {
let b = '';
res.on('data', c => b += c);
res.on('end', () => {
try { resolve({ status: res.statusCode, body: JSON.parse(b) }); }
catch (_) { resolve({ status: res.statusCode, body: b }); }
});
});
req.on('error', reject);
if (bodyStr) req.write(bodyStr);
req.end();
});
}
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 files = [];
if (Array.isArray(r.body)) {
for (const item of r.body) {
if (item.type === 'dir') files.push(...await collectFiles(item.path));
else files.push({ path: item.path, sha: item.sha });
}
} else if (r.body?.sha) {
files.push({ path: r.body.path, sha: r.body.sha });
}
return files;
}
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);
return r.body?.content ? r.body.content.replace(/\n/g, '') : '';
}
async function uploadFile(targetPath, contentBase64, message) {
// 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 body = { message, content: contentBase64, branch: 'HEAD' };
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);
}
async function deleteFile(filePath, sha) {
return giteaRequest('DELETE', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${filePath.split('/').map(encodeURIComponent).join('/')}`, {
message: `Delete ${filePath} (rename)`, sha, branch: 'HEAD'
});
}
// Collect all files under oldPath
const files = await collectFiles(oldPath);
// For each file: read content, upload to newPath, delete from oldPath
for (const f of files) {
const content = await readFileContent(f.path);
const relPath = isDir ? f.path.slice(oldPath.length + 1) : '';
const targetPath = isDir ? `${newPath}/${relPath}` : newPath;
await uploadFile(targetPath, content, `Rename: move ${f.path} to ${targetPath}`);
await deleteFile(f.path, f.sha);
}
return { ok: true };
} catch (e) {
console.error('rename-gitea-item error', e);
return { ok: false, error: String(e) };
}
});
// Gitea: Neue Datei oder Ordner (Ordner = Datei mit .gitkeep)
ipcMain.handle('create-gitea-item', async (event, data) => {
try {
const credentials = readCredentials();
const token = credentials?.giteaToken;
const giteaUrl = credentials?.giteaURL;
if (!token || !giteaUrl) return { ok: false, error: 'missing-token-or-url' };
const { owner, repo, path: itemPath, type } = data;
const urlObj = new URL(giteaUrl);
const protocol = urlObj.protocol === 'https:' ? https : http;
const port = urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80);
// Für Ordner: .gitkeep Datei anlegen
const targetPath = type === 'folder' ? `${itemPath}/.gitkeep` : itemPath;
const content = Buffer.from('').toString('base64');
return new Promise((resolve) => {
const body = JSON.stringify({ message: `Create ${itemPath}`, content, branch: data.branch || 'HEAD' });
const req = protocol.request({
hostname: urlObj.hostname, port,
path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${targetPath.split('/').map(encodeURIComponent).join('/')}`,
method: 'POST',
headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
}, (res) => {
let b = '';
res.on('data', c => b += c);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) resolve({ ok: true });
else resolve({ ok: false, error: `HTTP ${res.statusCode}: ${b}` });
});
});
req.on('error', e => resolve({ ok: false, error: String(e) }));
req.write(body);
req.end();
});
} catch (e) {
console.error('create-gitea-item error', e);
return { ok: false, error: String(e) };
}
});
// Lokal: Umbenennen
ipcMain.handle('rename-local-item', async (event, data) => {
try {
const { oldPath, newName } = data;
if (!oldPath || !fs.existsSync(oldPath)) return { ok: false, error: 'path-not-found' };
const dir = ppath.dirname(oldPath);
const newPath = ppath.join(dir, newName);
fs.renameSync(oldPath, newPath);
return { ok: true, newPath };
} catch (e) {
console.error('rename-local-item error', e);
return { ok: false, error: String(e) };
}
});
// Lokal: Neue Datei oder Ordner erstellen
ipcMain.handle('create-local-item', async (event, data) => {
try {
const { parentDir, name, type } = data;
const targetPath = ppath.join(parentDir, name);
if (type === 'folder') {
fs.mkdirSync(targetPath, { recursive: true });
} else {
// Sicherstellen dass Elternordner existiert
fs.mkdirSync(ppath.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, '', 'utf8');
}
return { ok: true, path: targetPath };
} catch (e) {
console.error('create-local-item error', e);
return { ok: false, error: String(e) };
}
});
// Lokal: Verschieben (Cut & Paste)
ipcMain.handle('move-local-item', async (event, data) => {
try {
const { srcPath, destDir } = data;
if (!srcPath || !fs.existsSync(srcPath)) return { ok: false, error: 'source-not-found' };
const name = ppath.basename(srcPath);
const destPath = ppath.join(destDir, name);
fs.mkdirSync(destDir, { recursive: true });
fs.renameSync(srcPath, destPath);
return { ok: true, destPath };
} catch (e) {
// renameSync kann über Laufwerke nicht funktionieren — dann cpSync + rmSync
try {
const { srcPath, destDir } = data;
const name = ppath.basename(srcPath);
const destPath = ppath.join(destDir, name);
if (fs.statSync(srcPath).isDirectory()) {
fs.cpSync(srcPath, destPath, { recursive: true });
} else {
fs.copyFileSync(srcPath, destPath);
}
fs.rmSync(srcPath, { recursive: true, force: true });
return { ok: true, destPath };
} catch (e2) {
console.error('move-local-item error', e2);
return { ok: false, error: String(e2) };
}
}
});
// Lokal: Kopieren
ipcMain.handle('copy-local-item', async (event, data) => {
try {
const { src, destDir } = data;
if (!src || !fs.existsSync(src)) return { ok: false, error: 'source-not-found' };
const name = ppath.basename(src);
const dest = ppath.join(destDir, name);
fs.mkdirSync(destDir, { recursive: true });
if (fs.statSync(src).isDirectory()) {
fs.cpSync(src, dest, { recursive: true });
} else {
fs.copyFileSync(src, dest);
}
return { ok: true, dest };
} catch (e) {
console.error('copy-local-item error', e);
return { ok: false, error: String(e) };
}
});
/* ================================
RELEASE MANAGEMENT IPC HANDLERS
================================ */
@@ -1592,6 +1934,57 @@ ipcMain.handle('get-commit-files', async (event, data) => {
/* ============================================
FAVORITEN & ZULETZT GEÖFFNET - Persistenz
============================================ */
function getFavoritesFilePath() {
return ppath.join(app.getPath('userData'), 'favorites.json');
}
function getRecentFilePath() {
return ppath.join(app.getPath('userData'), 'recent.json');
}
ipcMain.handle('load-favorites', async () => {
try {
const p = getFavoritesFilePath();
if (!fs.existsSync(p)) return { ok: true, favorites: [] };
return { ok: true, favorites: JSON.parse(fs.readFileSync(p, 'utf8')) || [] };
} catch (e) {
return { ok: true, favorites: [] };
}
});
ipcMain.handle('save-favorites', async (event, favorites) => {
try {
fs.writeFileSync(getFavoritesFilePath(), JSON.stringify(favorites || [], null, 2), 'utf8');
return { ok: true };
} catch (e) {
return { ok: false, error: String(e) };
}
});
ipcMain.handle('load-recent', async () => {
try {
const p = getRecentFilePath();
if (!fs.existsSync(p)) return { ok: true, recent: [] };
return { ok: true, recent: JSON.parse(fs.readFileSync(p, 'utf8')) || [] };
} catch (e) {
return { ok: true, recent: [] };
}
});
ipcMain.handle('save-recent', async (event, recent) => {
try {
const trimmed = (recent || []).slice(0, 20);
fs.writeFileSync(getRecentFilePath(), JSON.stringify(trimmed, null, 2), 'utf8');
return { ok: true };
} catch (e) {
return { ok: false, error: String(e) };
}
});
// main.js - Updater IPC Handlers
// 1. Version abfragen