Upload file main.js via GUI

This commit is contained in:
2025-12-28 18:21:41 +01:00
parent f7bb79c391
commit 6e82ec3ec4

946
main.js Normal file
View File

@@ -0,0 +1,946 @@
// main.js — Main-Process with concurrent folder upload/download, progress events, and temp-dir cleanup
const { app, BrowserWindow, ipcMain, dialog, Menu, nativeImage } = require('electron');
const ppath = require('path');
const fs = require('fs');
const os = require('os');
const crypto = require('crypto');
const { execSync } = require('child_process');
const https = require('https');
const http = require('http');
const {
createRepoGitHub,
createRepoGitea,
listGiteaRepos,
getGiteaRepoContents,
getGiteaFileContent,
uploadGiteaFile
} = require('./src/git/apiHandler.js');
const { initRepo, commitAndPush, getBranches, getCommitLogs } = require('./src/git/gitHandler.js');
const DATA_DIR = ppath.join(__dirname, 'data');
const CREDENTIALS_FILE = ppath.join(DATA_DIR, 'credentials.json');
const ALGORITHM = 'aes-256-cbc';
const SECRET_KEY = crypto.scryptSync('SuperSecretKey123!', 'salt', 32);
const IV = Buffer.alloc(16, 0);
// default concurrency for parallel uploads/downloads
const DEFAULT_CONCURRENCY = 4;
// temp drag cleanup delay (ms)
const TMP_CLEANUP_MS = 20_000;
function createWindow() {
// Entfernt das Menü (File, Edit, View...) komplett
Menu.setApplicationMenu(null);
const win = new BrowserWindow({
width: 1200,
height: 820,
webPreferences: {
preload: ppath.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true
}
});
win.loadFile(ppath.join(__dirname, 'renderer', 'index.html'));
// win.webContents.openDevTools();
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
});
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
/* -----------------------------
Helper: read credentials
----------------------------- */
function readCredentials() {
try {
if (!fs.existsSync(CREDENTIALS_FILE)) return null;
const encrypted = fs.readFileSync(CREDENTIALS_FILE);
const decipher = crypto.createDecipheriv(ALGORITHM, SECRET_KEY, IV);
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
return JSON.parse(decrypted.toString('utf8'));
} catch (e) {
console.error('readCredentials error', e);
return null;
}
}
/* -----------------------------
Generic concurrency runner (worker pool)
----------------------------- */
async function runLimited(tasks, concurrency = DEFAULT_CONCURRENCY, onProgress = null) {
const results = new Array(tasks.length);
let index = 0;
let processed = 0;
const total = tasks.length;
async function worker() {
while (true) {
const i = index++;
if (i >= tasks.length) return;
try {
const r = await tasks[i]();
results[i] = { ok: true, result: r };
} catch (e) {
results[i] = { ok: false, error: String(e) };
} finally {
processed++;
if (onProgress) {
try { onProgress(processed, total); } catch (_) {}
}
}
}
}
const workers = Array(Math.max(1, Math.min(concurrency, tasks.length))).fill(0).map(() => worker());
await Promise.all(workers);
return results;
}
/* -----------------------------
Basic IPC handlers
----------------------------- */
ipcMain.handle('select-folder', async () => {
const result = await dialog.showOpenDialog({ properties: ['openDirectory'] });
return result.canceled ? null : result.filePaths[0];
});
ipcMain.handle('select-file', async () => {
const result = await dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'] });
if (result.canceled) return { ok: false, files: [] };
return { ok: true, files: result.filePaths };
});
ipcMain.handle('save-credentials', async (event, data) => {
try {
fs.mkdirSync(DATA_DIR, { recursive: true });
const json = JSON.stringify(data);
const cipher = crypto.createCipheriv(ALGORITHM, SECRET_KEY, IV);
const encrypted = Buffer.concat([cipher.update(json, 'utf8'), cipher.final()]);
fs.writeFileSync(CREDENTIALS_FILE, encrypted);
return { ok: true };
} catch (e) {
console.error('save-credentials error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('load-credentials', async () => {
try {
return readCredentials();
} catch (e) {
console.error('load-credentials', e);
return null;
}
});
ipcMain.handle('create-repo', async (event, data) => {
try {
const credentials = readCredentials();
if (!credentials) return { ok: false, error: 'no-credentials' };
if (data.platform === 'github') {
const repo = await createRepoGitHub({
name: data.name,
token: credentials.githubToken,
auto_init: data.autoInit || true,
license: data.license || '',
private: data.private || false
});
return { ok: true, repo };
} else if (data.platform === 'gitea') {
const repo = await createRepoGitea({
name: data.name,
token: credentials.giteaToken,
url: credentials.giteaURL,
auto_init: data.autoInit || true,
license: data.license || '',
private: data.private || false
});
return { ok: true, repo };
} else return { ok: false, error: 'unknown-platform' };
} catch (e) {
console.error('create-repo error', e);
return { ok: false, error: String(e) };
}
});
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
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);
}
}
} catch (e) {
console.warn('Could not check local branch (maybe not a git repo yet)', e.message);
}
// 1. Git initialisieren, adden und commiten
await initRepo(data.folder);
// 2. Prüfen, ob ein 'origin' Remote existiert
let remoteUrl = '';
try {
remoteUrl = execSync('git remote get-url origin', { cwd: data.folder, stdio: 'pipe', encoding: 'utf-8' }).trim();
} catch (e) {
if (data.repoName && data.platform) {
const creds = readCredentials();
if (!creds) return { ok: false, error: 'credentials-missing-for-remote' };
const parts = data.repoName.split('/');
if (parts.length === 2) {
const owner = parts[0];
const repo = parts[1];
let constructedUrl = '';
if (data.platform === 'gitea' && creds.giteaURL) {
try {
const urlObj = new URL(creds.giteaURL);
constructedUrl = `${urlObj.protocol}//${creds.giteaToken}@${urlObj.host}/${owner}/${repo}.git`;
} catch (err) { console.error('Invalid Gitea URL', err); }
} else if (data.platform === 'github' && creds.githubToken) {
constructedUrl = `https://${creds.githubToken}@github.com/${owner}/${repo}.git`;
}
if (constructedUrl) {
try {
execSync(`git remote add origin "${constructedUrl}"`, { cwd: data.folder, stdio: 'inherit' });
console.log(`Remote origin added: ${constructedUrl}`);
} catch (err) {
return { ok: false, error: 'Failed to add remote origin: ' + String(err) };
}
} else {
return { ok: false, error: 'Could not construct remote URL.' };
}
} else {
return { ok: false, error: 'Use format Owner/RepoName.' };
}
}
}
// 3. Pushen (nutze 'main')
const progressCb = percent => { try { event.sender.send('push-progress', percent); } catch (_) {} };
await commitAndPush(data.folder, 'main', 'Update from Git Manager GUI', progressCb);
return { ok: true };
} catch (e) {
console.error('push-project error', e);
return { ok: false, error: String(e) };
}
});
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));
return { ok: true, branches };
} catch (e) {
console.error('getBranches error', e);
return { ok: false, error: String(e), branches: [] };
}
});
ipcMain.handle('getCommitLogs', async (event, data) => {
try {
const logs = await getCommitLogs(data.folder, data.count || 50);
return { ok: true, logs };
} catch (e) {
console.error('getCommitLogs error', e);
return { ok: false, error: String(e), logs: [] };
}
});
/* -----------------------------
Local file tree functions
----------------------------- */
function buildTree(dirPath, options = {}) {
const { exclude = ['node_modules'], maxDepth = 10 } = options;
function walk(currentPath, depth) {
let stat;
try { stat = fs.statSync(currentPath); } catch (e) { return null; }
const name = ppath.basename(currentPath);
const node = { name, path: currentPath, isDirectory: stat.isDirectory(), children: [], depth };
if (node.isDirectory) {
if (exclude.some(ex => currentPath.split(ppath.sep).includes(ex))) return null;
if (depth >= maxDepth) return node;
let items = [];
try { items = fs.readdirSync(currentPath); } catch (e) { return node; }
items.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
for (const it of items) {
const childPath = ppath.join(currentPath, it);
const child = walk(childPath, depth + 1);
if (child) node.children.push(child);
}
}
return node;
}
const roots = [];
const list = fs.readdirSync(dirPath).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
for (const entry of list) {
const full = ppath.join(dirPath, entry);
const n = walk(full, 0);
if (n) roots.push(n);
}
return roots;
}
ipcMain.handle('getFileTree', async (event, data) => {
try {
const folder = data && data.folder;
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 tree = buildTree(folder, opts);
return { ok: true, tree };
} catch (e) {
console.error('getFileTree error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('readFile', async (event, data) => {
try {
if (!data || !data.path || !fs.existsSync(data.path)) return { ok: false, error: 'file-not-found' };
const content = fs.readFileSync(data.path, 'utf8');
return { ok: true, content };
} catch (e) {
console.error('readFile error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('deleteFile', async (event, data) => {
try {
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 };
} catch (e) {
console.error('deleteFile error', e);
return { ok: false, error: String(e) };
}
});
/* -----------------------------
Gitea: list repos, contents, file content
----------------------------- */
ipcMain.handle('list-gitea-repos', async (event, data) => {
try {
const credentials = readCredentials();
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' };
const repos = await listGiteaRepos({ token, url });
return { ok: true, repos };
} catch (e) {
console.error('list-gitea-repos error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('get-gitea-repo-contents', async (event, data) => {
try {
const credentials = readCredentials();
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' };
const owner = data.owner;
const repo = data.repo;
const p = data.path || '';
// FIXED: Konvertiere 'master' zu 'main'
let ref = data.ref || 'main';
if (ref === 'master') ref = 'main';
const items = await getGiteaRepoContents({ token, url, owner, repo, path: p, ref });
return { ok: true, items };
} catch (e) {
console.error('get-gitea-repo-contents error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('get-gitea-file-content', async (event, data) => {
try {
const credentials = readCredentials();
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' };
const owner = data.owner;
const repo = data.repo;
const p = data.path;
// FIXED: Konvertiere 'master' zu 'main'
let ref = data.ref || 'main';
if (ref === 'master') ref = 'main';
const content = await getGiteaFileContent({ token, url, owner, repo, path: p, ref });
return { ok: true, content };
} catch (e) {
console.error('get-gitea-file-content error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('upload-gitea-file', async (event, data) => {
try {
const credentials = readCredentials();
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' };
const owner = data.owner;
const repo = data.repo;
// destPath is the target folder in the repo
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
// FIXED: Konvertiere 'master' zu 'main'
let branch = data.branch || 'main';
if (branch === 'master') branch = 'main';
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);
// FIXED: Handle destPath correctly. Always combine destPath + filename.
let targetPath;
if (destPath && destPath.length > 0) {
targetPath = `${destPath}/${fileName}`;
} else {
targetPath = fileName;
}
try {
const uploaded = await uploadGiteaFile({
token,
url,
owner,
repo,
path: targetPath,
contentBase64: base64,
message: `${message} - ${fileName}`,
branch
});
results.push({ file: localFile, ok: true, uploaded });
} catch (e) {
console.error('upload error for', localFile, e);
results.push({ file: localFile, ok: false, error: String(e) });
}
}
return { ok: true, results };
} catch (e) {
console.error('upload-gitea-file error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
try {
const credentials = readCredentials();
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' };
const localFolder = data.localFolder;
const owner = data.owner;
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';
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' };
const items = [];
const folderName = ppath.basename(localFolder);
// FIXED EXCLUDE LIST: Filter out .git, node_modules etc.
const excludeList = ['.git', 'node_modules', '.DS_Store', 'thumbs.db', '.vscode', '.idea'];
(function walk(dir) {
const entries = fs.readdirSync(dir);
for (const entry of entries) {
if (excludeList.includes(entry)) continue; // Skip excluded folders/files
const full = ppath.join(dir, entry);
let stat;
try {
stat = fs.statSync(full);
} catch(e) { continue; } // Skip unreadable files
if (stat.isDirectory()) {
walk(full);
} else if (stat.isFile()) {
const rel = ppath.relative(localFolder, full).split(ppath.sep).join('/');
// FIXED: Respect folder structure. Result: destPath/folderName/rel
let targetPath;
if (destPath && destPath.length > 0) {
targetPath = `${destPath}/${folderName}/${rel}`;
} else {
targetPath = `${folderName}/${rel}`;
}
items.push({ localFile: full, targetPath });
}
}
})(localFolder);
const total = items.length;
if (total === 0) return { ok: true, results: [] };
const tasks = items.map(it => async () => {
const raw = fs.readFileSync(it.localFile);
const base64 = raw.toString('base64');
return uploadGiteaFile({
token,
url,
owner,
repo,
path: it.targetPath,
contentBase64: base64,
message: `${messagePrefix} - ${ppath.basename(it.localFile)}`,
branch
});
});
const onProgress = (processed, t) => {
try { event.sender.send('folder-upload-progress', { processed, total: t, percent: Math.round((processed / t) * 100) }); } catch (_) {}
};
const results = await runLimited(tasks, concurrency, (proc, t) => onProgress(proc, total));
return { ok: true, results };
} catch (e) {
console.error('upload-local-folder-to-gitea error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('download-gitea-file', async (event, data) => {
try {
const credentials = readCredentials();
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' };
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 content = await getGiteaFileContent({ token, url, owner, repo, path: filePath, ref: 'main' });
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');
return { ok: true, savedTo: save.filePath };
} catch (e) {
console.error('download-gitea-file error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('download-gitea-folder', async (event, data) => {
try {
const credentials = readCredentials();
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' };
const owner = data.owner;
const repo = data.repo;
const remotePath = (data.path || '').replace(/^\/+/, '').replace(/\/+$/, '');
const concurrency = data.concurrency || DEFAULT_CONCURRENCY;
const result = await dialog.showOpenDialog({ properties: ['openDirectory', 'createDirectory'] });
if (result.canceled || !result.filePaths || result.filePaths.length === 0) return { ok: false, error: 'save-canceled' };
const destBase = result.filePaths[0];
const allFiles = [];
async function gather(pathInRepo) {
const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: 'main' });
for (const item of items) {
if (item.type === 'dir') await gather(item.path);
else if (item.type === 'file') allFiles.push(item.path);
}
}
await gather(remotePath || '');
const total = allFiles.length;
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 localPath = ppath.join(destBase, remoteFile);
fs.mkdirSync(ppath.dirname(localPath), { recursive: true });
fs.writeFileSync(localPath, content, 'utf8');
return localPath;
});
const onProgress = (processed, t) => {
try { event.sender.send('folder-download-progress', { processed, total: t, percent: Math.round((processed / t) * 100) }); } catch (_) {}
};
const results = await runLimited(tasks, concurrency, (proc, t) => onProgress(proc, total));
const savedFiles = results.filter(r => r.ok).map(r => r.result);
return { ok: true, savedTo: destBase, files: savedFiles };
} catch (e) {
console.error('download-gitea-folder error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('prepare-download-drag', async (event, data) => {
try {
const credentials = readCredentials();
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' };
const owner = data.owner;
const repo = data.repo;
const remotePath = (data.path || '').replace(/^\/+/, '').replace(/\/+$/, '');
const tmpBase = ppath.join(os.tmpdir(), repo);
if (fs.existsSync(tmpBase)) {
try { fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (e) { console.warn('Cleanup failed', e); }
}
fs.mkdirSync(tmpBase, { recursive: true });
const allFiles = [];
async function gather(pathInRepo) {
const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: 'main' });
for (const item of items) {
if (item.type === 'dir') await gather(item.path);
else if (item.type === 'file') allFiles.push(item.path);
}
}
await gather(remotePath || '');
const tasks = allFiles.map(remoteFile => async () => {
const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: 'main' });
const localPath = ppath.join(tmpBase, remoteFile);
fs.mkdirSync(ppath.dirname(localPath), { recursive: true });
fs.writeFileSync(localPath, content, 'utf8');
return localPath;
});
await runLimited(tasks, DEFAULT_CONCURRENCY);
setTimeout(() => { try { if (fs.existsSync(tmpBase)) fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (_) {} }, TMP_CLEANUP_MS);
return { ok: true, tempPath: tmpBase };
} catch (e) {
console.error('prepare-download-drag error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.on('ondragstart', async (event, filePath) => {
try {
if (!filePath || !fs.existsSync(filePath)) return;
let icon = nativeImage.createEmpty();
try { icon = await app.getFileIcon(filePath); } catch (e) {}
try { event.sender.startDrag({ file: filePath, icon: icon }); } catch (e) { console.error('startDrag failed', e); }
} catch (e) { console.error('ondragstart error', e); }
});
ipcMain.handle('delete-gitea-repo', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const urlStr = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !urlStr) return { ok: false, error: 'missing-token-or-url' };
const owner = data.owner;
const repo = data.repo;
const urlObj = new URL(urlStr);
const protocol = urlObj.protocol === 'https:' ? https : http;
const deletePath = `/api/v1/repos/${owner}/${repo}`;
return new Promise((resolve) => {
const options = {
hostname: urlObj.hostname,
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
path: deletePath,
method: 'DELETE',
headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' }
};
const req = protocol.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) resolve({ ok: true });
else resolve({ ok: false, error: `HTTP ${res.statusCode}: ${body}` });
});
});
req.on('error', (e) => resolve({ ok: false, error: String(e) }));
req.end();
});
} catch (e) {
console.error('delete-gitea-repo error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('upload-and-push', async (event, data) => {
try {
if (!data || !data.localFolder || !fs.existsSync(data.localFolder)) return { ok: false, error: 'local-folder-not-found' };
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
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';
const cloneUrl = data.cloneUrl || null;
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
if (!owner || !repo) return { ok: false, error: 'missing-owner-or-repo' };
// Prüfen ob es eine Datei oder ein Ordner ist
const stat = fs.statSync(data.localFolder);
const isFile = stat.isFile();
const isDirectory = stat.isDirectory();
// --- FALL 1: Einzelne Datei ---
if (isFile) {
console.log('Detected single file. Attempting API upload...');
const raw = fs.readFileSync(data.localFolder);
const base64 = raw.toString('base64');
const fileName = ppath.basename(data.localFolder);
// Pfad-Logik für Ziel
let targetPath;
if (destPath && destPath.length > 0) {
targetPath = `${destPath}/${fileName}`;
} else {
targetPath = fileName;
}
try {
// VERSUCH 1: API Upload
const result = await uploadGiteaFile({
token,
url: giteaUrl,
owner,
repo,
path: targetPath,
contentBase64: base64,
message: `Upload ${fileName} via GUI`,
branch
});
return { ok: true, usedGit: false, singleFile: true, uploaded: result };
} catch (e) {
console.error('Single file API upload error:', e.message);
// FALLBACK: Wenn API fehlschlägt (z.B. 422 SHA Fehler), nutze Git
// Git löst das "Überschreiben"-Problem zuverlässig.
console.log('Falling back to Git for single file...');
let finalCloneUrl = cloneUrl;
if (!finalCloneUrl && giteaUrl) {
try {
const base = giteaUrl.replace(/\/$/, '');
const urlObj = new URL(base);
finalCloneUrl = `${urlObj.protocol}//${urlObj.host}/${owner}/${repo}.git`;
} catch (err) { console.error('Invalid URL', err); }
}
if (!finalCloneUrl) return { ok: false, error: 'Cannot fallback to Git: No Clone URL' };
// Git-Workflow für einzelne Datei simulieren:
// 1. Temp Ordner erstellen
// 2. Repo klonen
// 3. Datei in Ordner kopieren
// 4. Commit & Push
let authClone = finalCloneUrl;
try {
const urlObj = new URL(finalCloneUrl);
if (token && (urlObj.protocol.startsWith('http'))) {
urlObj.username = encodeURIComponent(token);
authClone = urlObj.toString();
}
} catch (err) {}
const tmpDir = ppath.join(os.tmpdir(), `git-push-file-${owner}-${repo}-${Date.now()}`);
fs.mkdirSync(tmpDir, { recursive: true });
try {
execSync(`git clone --depth 1 --branch ${branch} "${authClone}" "${tmpDir}"`, { stdio: 'inherit' });
// Zielort im Repo bestimmen
let destDirInRepo = tmpDir;
if (destPath) {
destDirInRepo = ppath.join(tmpDir, destPath.split('/').join(ppath.sep));
fs.mkdirSync(destDirInRepo, { recursive: true });
}
const finalFileDest = ppath.join(destDirInRepo, fileName);
// Datei kopieren
fs.copyFileSync(data.localFolder, finalFileDest);
// Git Befehle
execSync(`git -C "${tmpDir}" add .`, { stdio: 'inherit' });
try { execSync(`git -C "${tmpDir}" commit -m "Upload file ${fileName} via GUI"`, { stdio: 'inherit' }); } catch (_) {}
execSync(`git -C "${tmpDir}" push origin ${branch}`, { stdio: 'inherit' });
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5_000);
return { ok: true, usedGit: true, singleFile: true, msg: 'Uploaded via Git (Fallback)' };
} catch (gitErr) {
console.error('Git Fallback failed:', gitErr);
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
// Wir werfen den ursprünglichen Fehler, da der Fallback auch gescheitert ist
return { ok: false, error: `API failed: ${e.message}. Git fallback failed: ${String(gitErr)}` };
}
}
}
// --- FALL 2: Ordner (Normale Git-Logik) ---
if (!isDirectory) {
return { ok: false, error: 'Path is neither file nor directory' };
}
let gitAvailable = true;
try { execSync('git --version', { stdio: 'ignore' }); } catch (e) { gitAvailable = false; }
let finalCloneUrl = cloneUrl;
if (!finalCloneUrl && giteaUrl) {
try {
const base = giteaUrl.replace(/\/$/, '');
const urlObj = new URL(base);
finalCloneUrl = `${urlObj.protocol}//${urlObj.host}/${owner}/${repo}.git`;
} catch (e) {}
}
if (gitAvailable && finalCloneUrl) {
let authClone = finalCloneUrl;
try {
const urlObj = new URL(finalCloneUrl);
if (token && (urlObj.protocol.startsWith('http'))) {
urlObj.username = encodeURIComponent(token);
authClone = urlObj.toString();
}
} catch (e) {}
const tmpDir = ppath.join(os.tmpdir(), `gitea-push-${owner}-${repo}-${Date.now()}`);
fs.mkdirSync(tmpDir, { recursive: true });
try {
execSync(`git clone --depth 1 --branch ${branch} "${authClone}" "${tmpDir}"`, { stdio: 'inherit' });
// FIXED: Respektieren von destPath und Ordnernamen im Git-Workflow
const folderName = ppath.basename(data.localFolder);
let targetBaseDir = tmpDir;
if (destPath) {
// Wenn ein Zielpfad angegeben ist (z.B. 'src'), erstelle diesen Ordner im Repo
const osDestPath = destPath.split('/').join(ppath.sep);
targetBaseDir = ppath.join(tmpDir, osDestPath);
fs.mkdirSync(targetBaseDir, { recursive: true });
}
// Ziel ist immer: targetBaseDir/folderName
const finalDest = ppath.join(targetBaseDir, folderName);
// FIXED: Korrekter Umgang mit fs.cpSync
// Wenn finalDest bereits existiert, muss es entfernt werden, damit cpSync funktioniert
if (fs.existsSync(finalDest)) {
fs.rmSync(finalDest, { recursive: true, force: true });
}
// Kopieren
if (fs.cpSync) {
fs.cpSync(data.localFolder, finalDest, { recursive: true, force: true });
} else {
if (process.platform === 'win32') {
execSync(`robocopy "${data.localFolder}" "${finalDest}" /E /NFL /NDL /NJH /NJS /nc /ns`, { stdio: 'inherit', shell: true });
} else {
execSync(`cp -r "${data.localFolder}/." "${finalDest}"`, { stdio: 'inherit', shell: true });
}
}
try {
execSync(`git -C "${tmpDir}" add .`, { stdio: 'inherit' });
try { execSync(`git -C "${tmpDir}" commit -m "Update from Git Manager GUI"`, { stdio: 'inherit' }); } catch (_) {}
execSync(`git -C "${tmpDir}" push origin ${branch}`, { stdio: 'inherit' });
} catch (e) { throw e; }
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5_000);
return { ok: true, usedGit: true, msg: 'Uploaded via git push' };
} catch (e) {
console.error('upload-and-push git-flow error', e);
try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
}
}
// Fallback: API Upload (paralleler Upload)
const items = [];
const folderName = ppath.basename(data.localFolder);
// FIXED EXCLUDE LIST: Filter out .git, node_modules etc.
const excludeList = ['.git', 'node_modules', '.DS_Store', 'thumbs.db', '.vscode', '.idea'];
(function walk(dir) {
const entries = fs.readdirSync(dir);
for (const entry of entries) {
if (excludeList.includes(entry)) continue; // Skip excluded folders/files
const full = ppath.join(dir, entry);
let stat;
try {
stat = fs.statSync(full);
} catch(e) { continue; }
if (stat.isDirectory()) {
walk(full);
} else if (stat.isFile()) {
const rel = ppath.relative(data.localFolder, full).split(ppath.sep).join('/');
// FIXED: Pfad-Respektierung: destPath/folderName/rel
let targetPath;
if (destPath && destPath.length > 0) {
targetPath = `${destPath}/${folderName}/${rel}`;
} else {
targetPath = `${folderName}/${rel}`;
}
items.push({ localFile: full, targetPath });
}
}
})(data.localFolder);
const total = items.length;
const concurrency = data.concurrency || DEFAULT_CONCURRENCY;
if (total === 0) return { ok: true, usedGit: false, results: [] };
const tasks = items.map(it => async () => {
const raw = fs.readFileSync(it.localFile);
const base64 = raw.toString('base64');
return uploadGiteaFile({
token,
url: giteaUrl,
owner,
repo,
path: it.targetPath,
contentBase64: base64,
message: `Upload via GUI - ${ppath.basename(it.localFile)}`,
branch
});
});
const onProgress = (processed, t) => {
try { event.sender.send('folder-upload-progress', { processed, total: t, percent: Math.round((processed / t) * 100) }); } catch (_) {}
};
const uploadResults = await runLimited(tasks, concurrency, (proc, t) => onProgress(proc, total));
return { ok: true, usedGit: false, results: uploadResults };
} catch (e) {
console.error('upload-and-push error', e);
return { ok: false, error: String(e) };
}
});