feat: add plugin download endpoint and improve download URL construction
This commit is contained in:
100
server/index.js
100
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) => {
|
app.get("/api/plugins/:owner/:repo", async (req, res) => {
|
||||||
const ownerRepo = `${req.params.owner}/${req.params.repo}`;
|
const ownerRepo = `${req.params.owner}/${req.params.repo}`;
|
||||||
try {
|
try {
|
||||||
const provider = (req.query.provider || "github").toLowerCase();
|
const provider = (req.query.provider || "github").toLowerCase();
|
||||||
const baseUrl = req.query.baseUrl || (provider === "github" ? "https://github.com" : "");
|
const baseUrlQuery = req.query.baseUrl || (provider === "github" ? "https://github.com" : "");
|
||||||
const entry = provider === "github" ? ownerRepo : { provider, repo: ownerRepo, baseUrl };
|
const entry = provider === "github" ? ownerRepo : { provider, repo: ownerRepo, baseUrl: baseUrlQuery };
|
||||||
const normalizedEntry = normalizeRepoInput(entry, { repo: ownerRepo, provider, baseUrl });
|
const normalizedEntry = normalizeRepoInput(entry, { repo: ownerRepo, provider, baseUrl: baseUrlQuery });
|
||||||
|
|
||||||
const info = await fetchRepo(entry);
|
const info = await fetchRepo(entry);
|
||||||
const [manifest, releases, commits] = await Promise.all([
|
const [manifest, releases, commits] = await Promise.all([
|
||||||
@@ -170,13 +189,28 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
|
|||||||
fetchCommits(entry).catch(() => [])
|
fetchCommits(entry).catch(() => [])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const effectiveBaseUrl =
|
||||||
|
normalizedEntry?.baseUrl ||
|
||||||
|
baseUrlQuery ||
|
||||||
|
info.baseUrl ||
|
||||||
|
(provider === "github" ? "https://github.com" : "");
|
||||||
|
|
||||||
const releasesWithDownload =
|
const releasesWithDownload =
|
||||||
normalizedEntry && releases.length > 0
|
normalizedEntry && releases.length > 0
|
||||||
? releases.map((release) => {
|
? releases.map((release) => {
|
||||||
const tagOrName = release.tag || release.name;
|
const tagOrName = release.tag || release.name;
|
||||||
|
if (!tagOrName) {
|
||||||
|
return release;
|
||||||
|
}
|
||||||
|
const endpoint = buildPluginDownloadEndpoint(ownerRepo, {
|
||||||
|
provider,
|
||||||
|
baseUrl: effectiveBaseUrl,
|
||||||
|
version: tagOrName
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
...release,
|
...release,
|
||||||
downloadUrl: tagOrName ? buildDownloadUrl(normalizedEntry, tagOrName, "release") : null
|
downloadUrl: endpoint,
|
||||||
|
sourceDownloadUrl: buildDownloadUrl(normalizedEntry, tagOrName, "release")
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
: releases;
|
: releases;
|
||||||
@@ -187,8 +221,14 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
|
|||||||
const sourceType = latestRelease ? "release" : "branch";
|
const sourceType = latestRelease ? "release" : "branch";
|
||||||
const version = latestRelease?.tag || latestRelease?.name || info.defaultBranch || "main";
|
const version = latestRelease?.tag || latestRelease?.name || info.defaultBranch || "main";
|
||||||
if (version) {
|
if (version) {
|
||||||
|
const endpoint = buildPluginDownloadEndpoint(ownerRepo, {
|
||||||
|
provider,
|
||||||
|
baseUrl: effectiveBaseUrl,
|
||||||
|
version
|
||||||
|
});
|
||||||
downloadMeta = {
|
downloadMeta = {
|
||||||
url: buildDownloadUrl(normalizedEntry, version, sourceType),
|
url: endpoint,
|
||||||
|
sourceUrl: buildDownloadUrl(normalizedEntry, version, sourceType),
|
||||||
version,
|
version,
|
||||||
sourceType
|
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) => {
|
app.get("/api/licenses", requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
let targetUserId = req.user.id;
|
let targetUserId = req.user.id;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export function buildDownloadUrl(repoEntry, version, sourceType = "release") {
|
|||||||
if (repoEntry.provider === "gitea") {
|
if (repoEntry.provider === "gitea") {
|
||||||
const sanitizedBase = (repoEntry.baseUrl || "").replace(/\/$/, "");
|
const sanitizedBase = (repoEntry.baseUrl || "").replace(/\/$/, "");
|
||||||
const [owner, repo] = ownerRepo.split("/");
|
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";
|
const refType = sourceType === "release" ? "tags" : "heads";
|
||||||
return `https://codeload.github.com/${ownerRepo}/zip/refs/${refType}/${version}`;
|
return `https://codeload.github.com/${ownerRepo}/zip/refs/${refType}/${version}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user