fix: serve git mirror with smart http

This commit is contained in:
Codex
2026-05-29 23:03:51 +08:00
parent 3e551d0c6b
commit 3d009a1e61
+139 -5
View File
@@ -2820,6 +2820,135 @@ fi
git -C "\${repo_path}" update-server-info
git -C "\${repo_path}" rev-parse refs/heads/v0.2 | tee /cache/HWLAB-v0.2.head
date -u +%Y-%m-%dT%H:%M:%SZ > /cache/HWLAB.last-sync
`,
"http.mjs": `import { createServer } from "node:http";
import { spawn } from "node:child_process";
import { createReadStream, statSync } from "node:fs";
import path from "node:path";
const cacheRoot = process.env.GIT_MIRROR_CACHE_ROOT || "/cache";
const port = Number(process.env.PORT || 8080);
createServer((req, res) => {
handle(req, res).catch((error) => {
if (!res.headersSent) res.writeHead(500, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: false, error: error.message }));
});
}).listen(port, "0.0.0.0", () => {
console.log(JSON.stringify({ event: "git-mirror-smart-http-listening", port, cacheRoot }));
});
async function handle(req, res) {
const url = new URL(req.url || "/", "http://git-mirror-http");
if (req.method === "GET" && url.pathname === "/") {
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-cache" });
res.end(JSON.stringify({ ok: true, service: "git-mirror-smart-http" }));
return;
}
if (req.method === "GET" && url.pathname.endsWith("/info/refs") && url.searchParams.get("service") === "git-upload-pack") {
await runUploadPackAdvertisement(repoPathFromGitServicePath(url.pathname, "/info/refs"), res);
return;
}
if (req.method === "POST" && url.pathname.endsWith("/git-upload-pack")) {
await runUploadPackRpc(repoPathFromGitServicePath(url.pathname, "/git-upload-pack"), req, res);
return;
}
if (req.method === "GET" || req.method === "HEAD") {
serveStaticFile(url.pathname, req, res);
return;
}
res.writeHead(405, { "content-type": "text/plain" });
res.end("method not allowed\n");
}
function repoPathFromGitServicePath(urlPath, suffix) {
const repoUrlPath = urlPath.slice(0, -suffix.length);
if (!repoUrlPath.endsWith(".git")) throw new Error(` + "`" + `unsupported git repo path: \${urlPath}` + "`" + `);
return safePath(repoUrlPath);
}
function safePath(urlPath) {
const decoded = decodeURIComponent(urlPath.replace(/^\/+/, ""));
const fullPath = path.resolve(cacheRoot, decoded);
const normalizedRoot = path.resolve(cacheRoot);
if (fullPath !== normalizedRoot && !fullPath.startsWith(` + "`" + `\${normalizedRoot}\${path.sep}` + "`" + `)) throw new Error("path escapes cache root");
return fullPath;
}
function smartHeaders(contentType) {
return {
"content-type": contentType,
"cache-control": "no-cache, max-age=0, must-revalidate",
expires: "Fri, 01 Jan 1980 00:00:00 GMT",
pragma: "no-cache"
};
}
function pktLine(text) {
const length = Buffer.byteLength(text) + 4;
return ` + "`" + `\${length.toString(16).padStart(4, "0")}\${text}` + "`" + `;
}
function runUploadPackAdvertisement(repoPath, res) {
return new Promise((resolve, reject) => {
const child = spawn("git-upload-pack", ["--stateless-rpc", "--advertise-refs", repoPath], { stdio: ["ignore", "pipe", "pipe"] });
let stderr = "";
child.stderr.on("data", (chunk) => { stderr += chunk; process.stderr.write(chunk); });
child.on("error", reject);
child.on("close", (code) => {
if (code !== 0 && !res.headersSent) reject(new Error(` + "`" + `git-upload-pack advertise failed: \${stderr.trim()}` + "`" + `));
else resolve();
});
res.writeHead(200, smartHeaders("application/x-git-upload-pack-advertisement"));
res.write(pktLine("# service=git-upload-pack\n"));
res.write("0000");
child.stdout.pipe(res, { end: false });
child.stdout.on("end", () => res.end());
});
}
function runUploadPackRpc(repoPath, req, res) {
return new Promise((resolve, reject) => {
const child = spawn("git-upload-pack", ["--stateless-rpc", repoPath], { stdio: ["pipe", "pipe", "pipe"] });
let stderr = "";
child.stderr.on("data", (chunk) => { stderr += chunk; process.stderr.write(chunk); });
child.on("error", reject);
child.on("close", (code) => {
if (code !== 0 && !res.headersSent) reject(new Error(` + "`" + `git-upload-pack rpc failed: \${stderr.trim()}` + "`" + `));
else resolve();
});
res.writeHead(200, smartHeaders("application/x-git-upload-pack-result"));
req.pipe(child.stdin);
child.stdout.pipe(res);
});
}
function serveStaticFile(urlPath, req, res) {
const fullPath = safePath(urlPath);
let stat;
try { stat = statSync(fullPath); } catch {
res.writeHead(404, { "content-type": "text/plain" });
res.end("not found\n");
return;
}
if (!stat.isFile()) {
res.writeHead(404, { "content-type": "text/plain" });
res.end("not found\n");
return;
}
res.writeHead(200, { "content-length": stat.size, "content-type": contentType(fullPath), "cache-control": "no-cache" });
if (req.method === "HEAD") {
res.end();
return;
}
createReadStream(fullPath).pipe(res);
}
function contentType(filePath) {
if (filePath.endsWith(".pack")) return "application/x-git-packed-objects";
if (filePath.endsWith(".idx")) return "application/x-git-packed-objects-toc";
return "text/plain";
}
`
}
},
@@ -2837,15 +2966,20 @@ date -u +%Y-%m-%dT%H:%M:%SZ > /cache/HWLAB.last-sync
name: "http",
image: ciToolsRunnerImage,
imagePullPolicy: "IfNotPresent",
workingDir: "/cache",
command: ["python3"],
args: ["-m", "http.server", "8080", "--bind", "0.0.0.0"],
command: ["node"],
args: ["/script/http.mjs"],
ports: [{ name: "http", containerPort: 8080 }],
readinessProbe: { httpGet: { path: "/pikasTech/HWLAB.git/HEAD", port: "http" }, initialDelaySeconds: 5, periodSeconds: 10 },
livenessProbe: { httpGet: { path: "/", port: "http" }, initialDelaySeconds: 10, periodSeconds: 30 },
volumeMounts: [{ name: "cache", mountPath: "/cache", readOnly: true }]
volumeMounts: [
{ name: "cache", mountPath: "/cache", readOnly: true },
{ name: "script", mountPath: "/script", readOnly: true }
]
}],
volumes: [{ name: "cache", persistentVolumeClaim: { claimName: "git-mirror-cache", readOnly: true } }]
volumes: [
{ name: "cache", persistentVolumeClaim: { claimName: "git-mirror-cache", readOnly: true } },
{ name: "script", configMap: { name: "git-mirror-sync-script", defaultMode: 0o555 } }
]
}
}
}