feat: add download functionality for licenses and enhance plugin detail view
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
import { Readable } from "node:stream";
|
||||
import {
|
||||
fetchCommits,
|
||||
fetchManifest,
|
||||
@@ -21,6 +22,7 @@ import { HOST, PATHS, PORT } from "./lib/config.js";
|
||||
import { ensureSchema } from "./lib/schema.js";
|
||||
import { authenticateUser, registerUser, adminCreateUser, listUsers, getUserById } from "./lib/userService.js";
|
||||
import { requireAuth, requireAdmin } from "./middleware/auth.js";
|
||||
import { getDownloadSourceForLicense, resolveRepoDownload, buildDownloadUrl } from "./lib/downloadService.js";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
@@ -159,6 +161,7 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
|
||||
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 info = await fetchRepo(entry);
|
||||
const [manifest, releases, commits] = await Promise.all([
|
||||
@@ -167,10 +170,36 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
|
||||
fetchCommits(entry).catch(() => [])
|
||||
]);
|
||||
|
||||
const releasesWithDownload =
|
||||
normalizedEntry && releases.length > 0
|
||||
? releases.map((release) => {
|
||||
const tagOrName = release.tag || release.name;
|
||||
return {
|
||||
...release,
|
||||
downloadUrl: tagOrName ? buildDownloadUrl(normalizedEntry, tagOrName, "release") : null
|
||||
};
|
||||
})
|
||||
: releases;
|
||||
|
||||
let downloadMeta = null;
|
||||
if (normalizedEntry) {
|
||||
const latestRelease = releasesWithDownload[0];
|
||||
const sourceType = latestRelease ? "release" : "branch";
|
||||
const version = latestRelease?.tag || latestRelease?.name || info.defaultBranch || "main";
|
||||
if (version) {
|
||||
downloadMeta = {
|
||||
url: buildDownloadUrl(normalizedEntry, version, sourceType),
|
||||
version,
|
||||
sourceType
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
...info,
|
||||
manifest,
|
||||
releases,
|
||||
releases: releasesWithDownload,
|
||||
download: downloadMeta,
|
||||
commits
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -283,6 +312,51 @@ app.post("/api/licenses/verify", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/licenses/download", async (req, res) => {
|
||||
try {
|
||||
const { key, hostname, version = "latest" } = req.body || {};
|
||||
if (!key || !hostname) {
|
||||
return res.status(400).json({ error: "Licentiecode en hostname zijn verplicht." });
|
||||
}
|
||||
const license = await findLicenseByKey(String(key).trim());
|
||||
if (!license) {
|
||||
return res.status(404).json({ error: "Licentie niet gevonden." });
|
||||
}
|
||||
const result = await touchLicenseHostname(license, hostname);
|
||||
if (!result.ok) {
|
||||
const status = result.conflict ? 403 : 400;
|
||||
return res.status(status).json({ error: result.error, boundHostname: license.primary_hostname });
|
||||
}
|
||||
const source = await getDownloadSourceForLicense(license, version);
|
||||
const remoteResponse = await fetch(source.url);
|
||||
if (!remoteResponse.ok || !remoteResponse.body) {
|
||||
return res.status(502).json({ error: "Kon plugin versie niet ophalen." });
|
||||
}
|
||||
|
||||
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("Download stream error", err);
|
||||
res.destroy(err);
|
||||
});
|
||||
stream.pipe(res);
|
||||
} catch (error) {
|
||||
console.error("Download endpoint error:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ error: "Download mislukt." });
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.use(express.static(PATHS.distDir));
|
||||
app.get("*", (_req, res) => {
|
||||
res.sendFile(path.join(PATHS.distDir, "index.html"));
|
||||
|
||||
65
server/lib/downloadService.js
Normal file
65
server/lib/downloadService.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { fetchRepo, fetchReleases, normalizeRepoInput } from "./pluginService.js";
|
||||
|
||||
export function buildDownloadUrl(repoEntry, version, sourceType = "release") {
|
||||
const ownerRepo = repoEntry.repo;
|
||||
if (repoEntry.provider === "gitea") {
|
||||
const sanitizedBase = (repoEntry.baseUrl || "").replace(/\/$/, "");
|
||||
const [owner, repo] = ownerRepo.split("/");
|
||||
return `${sanitizedBase}/repos/${owner}/${repo}/archive/${version}.zip`;
|
||||
}
|
||||
const refType = sourceType === "release" ? "tags" : "heads";
|
||||
return `https://codeload.github.com/${ownerRepo}/zip/refs/${refType}/${version}`;
|
||||
}
|
||||
|
||||
export async function resolveRepoDownload(repoEntry, requestedVersion = "latest") {
|
||||
const normalizedEntry = normalizeRepoInput(repoEntry, {
|
||||
repo: repoEntry.repo,
|
||||
provider: repoEntry.provider,
|
||||
baseUrl: repoEntry.baseUrl
|
||||
});
|
||||
if (!normalizedEntry) {
|
||||
throw new Error("Kon repository gegevens niet bepalen.");
|
||||
}
|
||||
|
||||
const repoInfo = await fetchRepo(normalizedEntry);
|
||||
const releases = await fetchReleases(normalizedEntry).catch(() => []);
|
||||
|
||||
let targetVersion = requestedVersion || "latest";
|
||||
let sourceType = "release";
|
||||
|
||||
if (targetVersion === "latest") {
|
||||
if (releases.length > 0) {
|
||||
targetVersion = releases[0].tag;
|
||||
} else {
|
||||
targetVersion = repoInfo.defaultBranch || "main";
|
||||
sourceType = "branch";
|
||||
}
|
||||
} else {
|
||||
const found = releases.find((release) => release.tag === targetVersion || release.name === targetVersion);
|
||||
if (!found) {
|
||||
sourceType = "branch";
|
||||
} else {
|
||||
targetVersion = found.tag;
|
||||
}
|
||||
}
|
||||
|
||||
const downloadUrl = buildDownloadUrl(normalizedEntry, targetVersion, sourceType);
|
||||
const filenameBase = repoInfo.name || normalizedEntry.repo.split("/").pop() || "plugin";
|
||||
return {
|
||||
url: downloadUrl,
|
||||
version: targetVersion,
|
||||
sourceType,
|
||||
filename: `${filenameBase}-${targetVersion}.zip`,
|
||||
repoEntry: normalizedEntry,
|
||||
repoInfo
|
||||
};
|
||||
}
|
||||
|
||||
export async function getDownloadSourceForLicense(licenseRow, requestedVersion = "latest") {
|
||||
const repoEntry = {
|
||||
repo: licenseRow.repo_name,
|
||||
provider: licenseRow.repo_provider,
|
||||
baseUrl: licenseRow.repo_base_url
|
||||
};
|
||||
return resolveRepoDownload(repoEntry, requestedVersion);
|
||||
}
|
||||
Reference in New Issue
Block a user