diff --git a/server/index.js b/server/index.js index a9fa4ef..dcdaa44 100644 --- a/server/index.js +++ b/server/index.js @@ -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; diff --git a/server/lib/downloadService.js b/server/lib/downloadService.js index 36854d1..34f6f36 100644 --- a/server/lib/downloadService.js +++ b/server/lib/downloadService.js @@ -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}`;