Upload file main.js via GUI
This commit is contained in:
946
main.js
Normal file
946
main.js
Normal 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) };
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user