Upload file main.js via GUI
This commit is contained in:
463
main.js
463
main.js
@@ -252,19 +252,13 @@ ipcMain.handle('push-project', async (event, data) => {
|
|||||||
try {
|
try {
|
||||||
if (!data.folder || !fs.existsSync(data.folder)) return { ok: false, error: 'folder-not-found' };
|
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 {
|
try {
|
||||||
const currentBranch = execSync('git branch --show-current', { cwd: data.folder, stdio: 'pipe', encoding: 'utf-8' }).trim();
|
const detected = execSync('git branch --show-current', { cwd: data.folder, stdio: 'pipe', encoding: 'utf-8' }).trim();
|
||||||
console.log('Current local branch:', currentBranch);
|
if (detected) {
|
||||||
|
currentBranch = currentBranch || detected;
|
||||||
if (currentBranch === 'master') {
|
console.log('Current local branch:', detected);
|
||||||
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) {
|
} catch (e) {
|
||||||
console.warn('Could not check local branch (maybe not a git repo yet)', e.message);
|
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 (_) {} };
|
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 };
|
return { ok: true };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('push-project error', e);
|
console.error('push-project error', e);
|
||||||
@@ -323,8 +319,20 @@ ipcMain.handle('push-project', async (event, data) => {
|
|||||||
ipcMain.handle('getBranches', async (event, data) => {
|
ipcMain.handle('getBranches', async (event, data) => {
|
||||||
try {
|
try {
|
||||||
const branches = await getBranches(data.folder);
|
const branches = await getBranches(data.folder);
|
||||||
// Sortieren, damit 'main' oben steht
|
// Aktuellen Branch ermitteln und nach oben sortieren
|
||||||
branches.sort((a, b) => (a === 'main' ? -1 : b === 'main' ?1 : 0));
|
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 };
|
return { ok: true, branches };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('getBranches error', e);
|
console.error('getBranches error', e);
|
||||||
@@ -463,6 +471,121 @@ ipcMain.handle('writeFile', async (event, data) => {
|
|||||||
|
|
||||||
ipcMain.handle('deleteFile', async (event, data) => {
|
ipcMain.handle('deleteFile', async (event, data) => {
|
||||||
try {
|
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' };
|
if (!data || !data.path || !fs.existsSync(data.path)) return { ok: false, error: 'file-not-found' };
|
||||||
fs.rmSync(data.path, { recursive: true, force: true });
|
fs.rmSync(data.path, { recursive: true, force: true });
|
||||||
return { ok: 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
|
// This allows apiHandler.js to try ['main', 'master'] if no ref is passed
|
||||||
const ref = data.ref;
|
const ref = data.ref;
|
||||||
|
|
||||||
const items = await getGiteaRepoContents({ token, url, owner, repo, path: p, ref });
|
const result = await getGiteaRepoContents({ token, url, owner, repo, path: p, ref });
|
||||||
return { ok: true, items };
|
return { ok: true, items: result.items || result, empty: result.empty || false };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('get-gitea-repo-contents error', e);
|
console.error('get-gitea-repo-contents error', e);
|
||||||
return { ok: false, error: String(e) };
|
return { ok: false, error: String(e) };
|
||||||
@@ -546,7 +669,7 @@ ipcMain.handle('read-gitea-file', async (event, data) => {
|
|||||||
const owner = data.owner;
|
const owner = data.owner;
|
||||||
const repo = data.repo;
|
const repo = data.repo;
|
||||||
const p = data.path;
|
const p = data.path;
|
||||||
const ref = data.ref || 'main';
|
const ref = data.ref || 'HEAD';
|
||||||
|
|
||||||
console.log(`read-gitea-file: ${owner}/${repo}/${p} (ref: ${ref})`);
|
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;
|
const repo = data.repo;
|
||||||
// destPath is the target folder in the repo
|
// destPath is the target folder in the repo
|
||||||
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
|
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
|
||||||
// FIXED: Konvertiere 'master' zu 'main' (Upload should generally target main)
|
// Branch wird unverändert übernommen (main UND master werden unterstützt)
|
||||||
let branch = data.branch || 'main';
|
let branch = data.branch || 'HEAD';
|
||||||
if (branch === 'master') branch = 'main';
|
|
||||||
const message = data.message || 'Upload via Git Manager GUI';
|
const message = data.message || 'Upload via Git Manager GUI';
|
||||||
const localFiles = Array.isArray(data.localPath) ? data.localPath : (data.localPath ? [data.localPath] : []);
|
const localFiles = Array.isArray(data.localPath) ? data.localPath : (data.localPath ? [data.localPath] : []);
|
||||||
const results = [];
|
const results = [];
|
||||||
@@ -678,7 +800,7 @@ ipcMain.handle('write-gitea-file', async (event, data) => {
|
|||||||
const repo = data.repo;
|
const repo = data.repo;
|
||||||
const path = data.path;
|
const path = data.path;
|
||||||
const content = data.content || '';
|
const content = data.content || '';
|
||||||
const ref = data.ref || 'main';
|
const ref = data.ref || 'HEAD';
|
||||||
|
|
||||||
// Konvertiere Content zu Base64
|
// Konvertiere Content zu Base64
|
||||||
const base64 = Buffer.from(content, 'utf8').toString('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;
|
const repo = data.repo;
|
||||||
// destPath is the target directory in the repo
|
// destPath is the target directory in the repo
|
||||||
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
|
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
|
||||||
// FIXED: Konvertiere 'master' zu 'main'
|
// Branch wird unverändert übernommen (main UND master werden unterstützt)
|
||||||
let branch = data.branch || 'main';
|
let branch = data.branch || 'HEAD';
|
||||||
if (branch === 'master') branch = 'main';
|
|
||||||
const messagePrefix = data.messagePrefix || 'Upload folder via GUI';
|
const messagePrefix = data.messagePrefix || 'Upload folder via GUI';
|
||||||
const concurrency = data.concurrency || DEFAULT_CONCURRENCY;
|
const concurrency = data.concurrency || DEFAULT_CONCURRENCY;
|
||||||
if (!localFolder || !fs.existsSync(localFolder)) return { ok: false, error: 'local-folder-not-found' };
|
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 repo = data.repo;
|
||||||
const filePath = data.path;
|
const filePath = data.path;
|
||||||
if (!owner || !repo || !filePath) return { ok: false, error: 'missing-owner-repo-or-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) });
|
const save = await dialog.showSaveDialog({ defaultPath: ppath.basename(filePath) });
|
||||||
if (save.canceled || !save.filePath) return { ok: false, error: 'save-canceled' };
|
if (save.canceled || !save.filePath) return { ok: false, error: 'save-canceled' };
|
||||||
fs.writeFileSync(save.filePath, content, 'utf8');
|
fs.writeFileSync(save.filePath, content, 'utf8');
|
||||||
@@ -820,7 +941,8 @@ ipcMain.handle('download-gitea-folder', async (event, data) => {
|
|||||||
|
|
||||||
const allFiles = [];
|
const allFiles = [];
|
||||||
async function gather(pathInRepo) {
|
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) {
|
for (const item of items) {
|
||||||
if (item.type === 'dir') await gather(item.path);
|
if (item.type === 'dir') await gather(item.path);
|
||||||
else if (item.type === 'file') allFiles.push(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: [] };
|
if (total === 0) return { ok: true, savedTo: destBase, files: [] };
|
||||||
|
|
||||||
const tasks = allFiles.map(remoteFile => async () => {
|
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);
|
const localPath = ppath.join(destBase, remoteFile);
|
||||||
fs.mkdirSync(ppath.dirname(localPath), { recursive: true });
|
fs.mkdirSync(ppath.dirname(localPath), { recursive: true });
|
||||||
fs.writeFileSync(localPath, content, 'utf8');
|
fs.writeFileSync(localPath, content, 'utf8');
|
||||||
@@ -892,8 +1014,9 @@ ipcMain.handle('prepare-download-drag', async (event, data) => {
|
|||||||
// Gather list of files (recursive)
|
// Gather list of files (recursive)
|
||||||
const allFiles = [];
|
const allFiles = [];
|
||||||
async function gather(pathInRepo) {
|
async function gather(pathInRepo) {
|
||||||
const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'main' });
|
const _r2 = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'HEAD' });
|
||||||
for (const item of items || []) {
|
const items = (_r2.items || _r2) || [];
|
||||||
|
for (const item of items) {
|
||||||
if (item.type === 'dir') await gather(item.path);
|
if (item.type === 'dir') await gather(item.path);
|
||||||
else if (item.type === 'file') allFiles.push(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:
|
// Download files sequentially or with limited concurrency:
|
||||||
const tasks = allFiles.map(remoteFile => async () => {
|
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);
|
const localPath = ppath.join(tmpBase, remoteFile);
|
||||||
ensureDir(ppath.dirname(localPath));
|
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 giteaUrl = (data && data.url) || (credentials && credentials.giteaURL);
|
||||||
const owner = data.owner;
|
const owner = data.owner;
|
||||||
const repo = data.repo;
|
const repo = data.repo;
|
||||||
// FIXED: Konvertiere 'master' zu 'main'
|
// Branch wird unverändert übernommen (main UND master werden unterstützt)
|
||||||
let branch = data.branch || 'main';
|
let branch = data.branch || 'HEAD';
|
||||||
if (branch === 'master') branch = 'main';
|
|
||||||
const cloneUrl = data.cloneUrl || null;
|
const cloneUrl = data.cloneUrl || null;
|
||||||
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
|
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
|
||||||
if (!owner || !repo) return { ok: false, error: 'missing-owner-or-repo' };
|
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) };
|
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
|
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
|
// main.js - Updater IPC Handlers
|
||||||
|
|
||||||
// 1. Version abfragen
|
// 1. Version abfragen
|
||||||
|
|||||||
Reference in New Issue
Block a user