From 658b29368b09e5dc853073c9e7d46ec17cea83b1 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Tue, 24 Mar 2026 16:34:42 +0100 Subject: [PATCH] Update from Git Manager GUI --- src/git/apiHandler.js | 142 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 115 insertions(+), 27 deletions(-) diff --git a/src/git/apiHandler.js b/src/git/apiHandler.js index 88b9d05..d1486a6 100644 --- a/src/git/apiHandler.js +++ b/src/git/apiHandler.js @@ -3,10 +3,96 @@ // getGiteaRepoContents, getGiteaFileContent, uploadGiteaFile const axios = require('axios'); +const http = require('http'); +const https = require('https'); + +// IPv4 bevorzugen – verhindert ETIMEDOUT wenn der Hostname nur per IPv6 erreichbar wäre +// oder Node.js fälschlicherweise IPv6 vorranging versucht. +const ipv4HttpAgent = new http.Agent({ family: 4, keepAlive: true }); +const ipv4HttpsAgent = new https.Agent({ family: 4, keepAlive: true }); + +const axiosInstance = axios.create({ + httpAgent: ipv4HttpAgent, + httpsAgent: ipv4HttpsAgent, +}); + +function normalizeAndValidateBaseUrl(rawUrl) { + const value = (rawUrl || '').trim(); + if (!value) { + throw new Error('Gitea URL fehlt. Bitte tragen Sie eine URL ein.'); + } + + let parsed; + try { + parsed = new URL(value); + } catch (_) { + throw new Error('Ungueltige Gitea URL. Beispiel fuer IPv6: http://[2001:db8::1]:3000'); + } + + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + throw new Error('Gitea URL muss mit http:// oder https:// beginnen.'); + } + + return value.replace(/\/$/, ''); +} function normalizeBase(url) { if (!url) return null; - return url.replace(/\/$/, ''); + return normalizeAndValidateBaseUrl(url); +} + +async function checkGiteaConnection({ token, url, timeout = 8000 }) { + const base = normalizeAndValidateBaseUrl(url); + const started = Date.now(); + + const versionRes = await axiosInstance.get(`${base}/api/v1/version`, { + timeout, + validateStatus: () => true, + headers: { + 'User-Agent': 'Git-Manager-GUI' + } + }); + + const latencyMs = Math.max(1, Date.now() - started); + const apiReachable = versionRes.status >= 200 && versionRes.status < 500; + + let authStatus = null; + let authOk = false; + if (token) { + const userRes = await axiosInstance.get(`${base}/api/v1/user`, { + timeout, + validateStatus: () => true, + headers: { + Authorization: `token ${token}`, + 'User-Agent': 'Git-Manager-GUI' + } + }); + authStatus = userRes.status; + authOk = userRes.status >= 200 && userRes.status < 300; + } + + const serverVersion = + (versionRes.data && (versionRes.data.version || versionRes.data.Version || versionRes.data.tag)) || + null; + + return { + ok: apiReachable && (!!token ? authOk : true), + base, + checks: { + urlValid: true, + apiReachable, + authProvided: !!token, + authOk + }, + metrics: { + latencyMs, + versionStatus: versionRes.status, + authStatus + }, + server: { + version: serverVersion + } + }; } function buildContentsUrl(base, owner, repo, p) { @@ -17,7 +103,7 @@ function buildContentsUrl(base, owner, repo, p) { async function tryRequest(url, token, opts = {}) { try { - const res = await axios.get(url, { + const res = await axiosInstance.get(url, { headers: token ? { Authorization: `token ${token}` } : {}, timeout: opts.timeout || 10000 }); @@ -40,7 +126,7 @@ async function createRepoGitHub({ name, token, auto_init = true, license = '', p } try { - const response = await axios.post('https://api.github.com/user/repos', body, { + const response = await axiosInstance.post('https://api.github.com/user/repos', body, { headers: { Authorization: `token ${token}` } }); return response.data; @@ -99,7 +185,7 @@ async function createRepoGitea({ name, token, url, auto_init = true, license = ' console.log('Request body:', JSON.stringify(body, null, 2)); try { - const response = await axios.post(endpoint, body, { + const response = await axiosInstance.post(endpoint, body, { headers: { Authorization: `token ${token}` }, timeout: 15000 // 15 Sekunden Timeout }); @@ -122,7 +208,7 @@ async function createRepoGitea({ name, token, url, auto_init = true, license = ' }; try { - const retryResponse = await axios.post(endpoint, bodyWithoutLicense, { + const retryResponse = await axiosInstance.post(endpoint, bodyWithoutLicense, { headers: { Authorization: `token ${token}` }, timeout: 15000 }); @@ -168,7 +254,7 @@ async function createRepoGitea({ name, token, url, auto_init = true, license = ' async function listGiteaRepos({ token, url }) { const endpoint = normalizeBase(url) + '/api/v1/user/repos'; - const response = await axios.get(endpoint, { + const response = await axiosInstance.get(endpoint, { headers: { Authorization: `token ${token}` } }); return response.data; @@ -335,8 +421,9 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m const fetchSha = async () => { try { const existing = await getGiteaRepoContents({ token, url: base, owner, repo, path, ref: branchName }); - if (existing && existing.length > 0 && existing[0].sha) { - return existing[0].sha; + const items = existing && existing.items ? existing.items : (Array.isArray(existing) ? existing : []); + if (items.length > 0 && items[0].sha) { + return items[0].sha; } return null; } catch (e) { @@ -350,11 +437,10 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m const fileName = pathParts.pop(); const dirPath = pathParts.join('/'); - const list = await getGiteaRepoContents({ token, url: base, owner, repo, path: dirPath, ref: branchName }); - if (Array.isArray(list)) { - const item = list.find(i => i.name === fileName); - if (item && item.sha) return item.sha; - } + const result = await getGiteaRepoContents({ token, url: base, owner, repo, path: dirPath, ref: branchName }); + const list = result && result.items ? result.items : (Array.isArray(result) ? result : []); + const item = list.find(i => i.name === fileName); + if (item && item.sha) return item.sha; return null; } catch (e) { return null; @@ -385,7 +471,7 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m console.log(`[Upload Debug] Datei: ${path}, Branch: ${branchName}, SHA: ${sha ? sha.substring(0, 8) : 'keine'}`); try { - const res = await axios.put(endpoint, body, { + const res = await axiosInstance.put(endpoint, body, { headers: { Authorization: `token ${token}` }, timeout: 30000 // 30 Sekunden Timeout für größere Dateien }); @@ -478,7 +564,7 @@ async function getGiteaCommits({ token, url, owner, repo, branch = 'HEAD', page const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits`; try { - const response = await axios.get(endpoint, { + const response = await axiosInstance.get(endpoint, { headers: { Authorization: `token ${token}` }, params: { sha: branch, @@ -503,7 +589,7 @@ async function getGiteaCommit({ token, url, owner, repo, sha }) { const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/commits/${sha}`; try { - const response = await axios.get(endpoint, { + const response = await axiosInstance.get(endpoint, { headers: { Authorization: `token ${token}` } }); return response.data; @@ -524,7 +610,7 @@ async function getGiteaCommitDiff({ token, url, owner, repo, sha }) { const endpoint = `${base}/${owner}/${repo}/commit/${sha}.diff`; try { - const response = await axios.get(endpoint, { + const response = await axiosInstance.get(endpoint, { headers: { Authorization: `token ${token}` } }); return response.data; @@ -544,7 +630,7 @@ async function getGiteaCommitFiles({ token, url, owner, repo, sha }) { const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/commits/${sha}`; try { - const response = await axios.get(endpoint, { + const response = await axiosInstance.get(endpoint, { headers: { Authorization: `token ${token}` } }); @@ -581,7 +667,7 @@ async function searchGiteaCommits({ token, url, owner, repo, query, branch = 'HE const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits`; try { - const response = await axios.get(endpoint, { + const response = await axiosInstance.get(endpoint, { headers: { Authorization: `token ${token}` }, params: { sha: branch, @@ -614,7 +700,7 @@ async function getGiteaBranches({ token, url, owner, repo }) { const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches`; try { - const response = await axios.get(endpoint, { + const response = await axiosInstance.get(endpoint, { headers: { Authorization: `token ${token}` } }); return response.data; @@ -638,7 +724,7 @@ async function listGiteaReleases({ token, url, owner, repo }) { const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases`; try { - const response = await axios.get(endpoint, { + const response = await axiosInstance.get(endpoint, { headers: { Authorization: `token ${token}` } }); return response.data; @@ -658,7 +744,7 @@ async function getGiteaRelease({ token, url, owner, repo, tag }) { const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`; try { - const response = await axios.get(endpoint, { + const response = await axiosInstance.get(endpoint, { headers: { Authorization: `token ${token}` } }); return response.data; @@ -687,7 +773,7 @@ async function createGiteaRelease({ token, url, owner, repo, data }) { }; try { - const response = await axios.post(endpoint, body, { + const response = await axiosInstance.post(endpoint, body, { headers: { Authorization: `token ${token}`, 'Content-Type': 'application/json' @@ -747,7 +833,7 @@ async function editGiteaRelease({ token, url, owner, repo, releaseId, data }) { if (data.tag_name !== undefined) body.tag_name = data.tag_name; try { - const response = await axios.patch(endpoint, body, { + const response = await axiosInstance.patch(endpoint, body, { headers: { Authorization: `token ${token}`, 'Content-Type': 'application/json' @@ -770,7 +856,7 @@ async function deleteGiteaRelease({ token, url, owner, repo, releaseId }) { const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${releaseId}`; try { - await axios.delete(endpoint, { + await axiosInstance.delete(endpoint, { headers: { Authorization: `token ${token}` } }); return { ok: true }; @@ -799,7 +885,7 @@ async function uploadReleaseAsset({ token, url, owner, repo, releaseId, filePath }); try { - const response = await axios.post(endpoint, formData, { + const response = await axiosInstance.post(endpoint, formData, { headers: { Authorization: `token ${token}`, ...formData.getHeaders() @@ -824,7 +910,7 @@ async function deleteReleaseAsset({ token, url, owner, repo, assetId }) { const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/assets/${assetId}`; try { - await axios.delete(endpoint, { + await axiosInstance.delete(endpoint, { headers: { Authorization: `token ${token}` } }); return { ok: true }; @@ -835,8 +921,10 @@ async function deleteReleaseAsset({ token, url, owner, repo, assetId }) { } module.exports = { + normalizeAndValidateBaseUrl, createRepoGitHub, createRepoGitea, + checkGiteaConnection, listGiteaRepos, getGiteaRepoContents, getGiteaFileContent,