feat: add plugin download endpoint and improve download URL construction

This commit is contained in:
2026-02-01 02:49:06 +00:00
parent 65b0de63a9
commit 2473f3f708
2 changed files with 96 additions and 6 deletions

View File

@@ -155,13 +155,32 @@ app.get("/api/plugins", async (_req, res) => {
}
});
function buildPluginDownloadEndpoint(ownerRepo, { provider, baseUrl, version } = {}) {
const [owner, repo] = ownerRepo.split("/");
if (!owner || !repo) {
return null;
}
const params = new URLSearchParams();
if (provider) {
params.set("provider", provider);
}
if (baseUrl) {
params.set("baseUrl", baseUrl);
}
if (version) {
params.set("version", version);
}
const search = params.toString();
return `/api/plugins/${owner}/${repo}/download${search ? `?${search}` : ""}`;
}
app.get("/api/plugins/:owner/:repo", async (req, res) => {
const ownerRepo = `${req.params.owner}/${req.params.repo}`;
try {
const provider = (req.query.provider || "github").toLowerCase();
const baseUrl = req.query.baseUrl || (provider === "github" ? "https://github.com" : "");
const entry = provider === "github" ? ownerRepo : { provider, repo: ownerRepo, baseUrl };
const normalizedEntry = normalizeRepoInput(entry, { repo: ownerRepo, provider, baseUrl });
const baseUrlQuery = req.query.baseUrl || (provider === "github" ? "https://github.com" : "");
const entry = provider === "github" ? ownerRepo : { provider, repo: ownerRepo, baseUrl: baseUrlQuery };
const normalizedEntry = normalizeRepoInput(entry, { repo: ownerRepo, provider, baseUrl: baseUrlQuery });
const info = await fetchRepo(entry);
const [manifest, releases, commits] = await Promise.all([
@@ -170,13 +189,28 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
fetchCommits(entry).catch(() => [])
]);
const effectiveBaseUrl =
normalizedEntry?.baseUrl ||
baseUrlQuery ||
info.baseUrl ||
(provider === "github" ? "https://github.com" : "");
const releasesWithDownload =
normalizedEntry && releases.length > 0
? releases.map((release) => {
const tagOrName = release.tag || release.name;
if (!tagOrName) {
return release;
}
const endpoint = buildPluginDownloadEndpoint(ownerRepo, {
provider,
baseUrl: effectiveBaseUrl,
version: tagOrName
});
return {
...release,
downloadUrl: tagOrName ? buildDownloadUrl(normalizedEntry, tagOrName, "release") : null
downloadUrl: endpoint,
sourceDownloadUrl: buildDownloadUrl(normalizedEntry, tagOrName, "release")
};
})
: releases;
@@ -187,8 +221,14 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
const sourceType = latestRelease ? "release" : "branch";
const version = latestRelease?.tag || latestRelease?.name || info.defaultBranch || "main";
if (version) {
const endpoint = buildPluginDownloadEndpoint(ownerRepo, {
provider,
baseUrl: effectiveBaseUrl,
version
});
downloadMeta = {
url: buildDownloadUrl(normalizedEntry, version, sourceType),
url: endpoint,
sourceUrl: buildDownloadUrl(normalizedEntry, version, sourceType),
version,
sourceType
};
@@ -207,6 +247,56 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
}
});
app.get("/api/plugins/:owner/:repo/download", async (req, res) => {
const ownerRepo = `${req.params.owner}/${req.params.repo}`;
try {
const provider = (req.query.provider || "github").toLowerCase();
const baseUrl = req.query.baseUrl || undefined;
const version = req.query.version || "latest";
if (provider === "gitea" && !baseUrl) {
return res.status(400).json({ error: "Gitea downloads vereisen een baseUrl." });
}
const repoEntry = normalizeRepoInput(
{ repo: ownerRepo, provider, baseUrl },
{ repo: ownerRepo, provider, baseUrl }
);
if (!repoEntry) {
return res.status(400).json({ error: "Ongeldige repository." });
}
const source = await resolveRepoDownload(repoEntry, version);
const remoteResponse = await fetch(source.url);
if (!remoteResponse.ok || !remoteResponse.body) {
return res.status(502).json({ error: "Kon plugin versie niet downloaden." });
}
res.setHeader("Content-Type", remoteResponse.headers.get("content-type") || "application/zip");
const length = remoteResponse.headers.get("content-length");
if (length) {
res.setHeader("Content-Length", length);
}
res.setHeader("Content-Disposition", `attachment; filename="${source.filename}"`);
res.setHeader("X-Plugin-Version", source.version);
const stream = Readable.fromWeb(remoteResponse.body);
stream.on("error", (err) => {
console.error("Plugin download stream error:", err);
res.destroy(err);
});
stream.pipe(res);
} catch (error) {
console.error("Plugin download endpoint error:", error);
if (!res.headersSent) {
res.status(500).json({ error: "Download mislukt." });
} else {
res.end();
}
}
});
app.get("/api/licenses", requireAuth, async (req, res) => {
try {
let targetUserId = req.user.id;

View File

@@ -5,7 +5,7 @@ export function buildDownloadUrl(repoEntry, version, sourceType = "release") {
if (repoEntry.provider === "gitea") {
const sanitizedBase = (repoEntry.baseUrl || "").replace(/\/$/, "");
const [owner, repo] = ownerRepo.split("/");
return `${sanitizedBase}/repos/${owner}/${repo}/archive/${version}.zip`;
return `${sanitizedBase}/${owner}/${repo}/archive/${version}.zip`;
}
const refType = sourceType === "release" ? "tags" : "heads";
return `https://codeload.github.com/${ownerRepo}/zip/refs/${refType}/${version}`;