Add software version manifest management
This commit is contained in:
+354
-18
@@ -13,6 +13,7 @@ const dataDir = path.join(__dirname, "data");
|
||||
const uploadDir = path.join(__dirname, "uploads");
|
||||
const packageDir = path.join(uploadDir, "packages");
|
||||
const iconDir = path.join(uploadDir, "icons");
|
||||
const manifestDir = path.join(uploadDir, "manifests");
|
||||
const dataFile = path.join(dataDir, "software.json");
|
||||
const playerFile = path.join(dataDir, "players.json");
|
||||
const port = Number(process.env.PORT || 3030);
|
||||
@@ -67,6 +68,7 @@ async function ensureStorage() {
|
||||
fsp.mkdir(dataDir, { recursive: true }),
|
||||
fsp.mkdir(packageDir, { recursive: true }),
|
||||
fsp.mkdir(iconDir, { recursive: true }),
|
||||
fsp.mkdir(manifestDir, { recursive: true }),
|
||||
]);
|
||||
try {
|
||||
await fsp.access(dataFile);
|
||||
@@ -153,6 +155,40 @@ async function hashFile(filePath) {
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
async function readJsonFile(filePath) {
|
||||
try {
|
||||
return parseJsonObject((await fsp.readFile(filePath, "utf8")).replace(/^\uFEFF/, ""), "version.json");
|
||||
} catch (error) {
|
||||
if (error.status) throw error;
|
||||
const wrapped = new Error(`Invalid version.json: ${error.message}`);
|
||||
wrapped.status = 400;
|
||||
throw wrapped;
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonObject(raw, label) {
|
||||
const parsed = JSON.parse(String(raw || "").replace(/^\uFEFF/, ""));
|
||||
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
|
||||
const error = new Error(`${label} must be a JSON object`);
|
||||
error.status = 400;
|
||||
throw error;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseVersionManifestContent(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return parseJsonObject(raw, "versionManifestContent");
|
||||
} catch (error) {
|
||||
if (error.status) throw error;
|
||||
const wrapped = new Error(`Invalid versionManifestContent: ${error.message}`);
|
||||
wrapped.status = 400;
|
||||
throw wrapped;
|
||||
}
|
||||
}
|
||||
|
||||
function hashPassword(password, salt = crypto.randomBytes(16).toString("hex")) {
|
||||
const hash = crypto.pbkdf2Sync(String(password), salt, passwordIterations, 32, "sha256").toString("hex");
|
||||
return `pbkdf2_sha256$${passwordIterations}$${salt}$${hash}`;
|
||||
@@ -254,7 +290,12 @@ function publicItem(item, includePrivate = false) {
|
||||
if (!includePrivate) {
|
||||
delete copy.storagePath;
|
||||
delete copy.iconStoragePath;
|
||||
delete copy.versionManifestStoragePath;
|
||||
delete copy.externalDownloadUrl;
|
||||
delete copy.versionManifest;
|
||||
copy.hasVersionManifest = Boolean(item.versionManifest);
|
||||
copy.versionManifestUrl = `/api/software/${encodeURIComponent(item.id)}/version.json`;
|
||||
copy.checkUpdateUrl = `/api/software/${encodeURIComponent(item.id)}/check-update`;
|
||||
if (resolveDownloadUrl(item)) {
|
||||
copy.downloadUrl = `/api/software/${encodeURIComponent(item.id)}/download`;
|
||||
}
|
||||
@@ -274,6 +315,7 @@ async function makeFilePatch(files = {}) {
|
||||
const patch = {};
|
||||
const packageFile = files.package?.[0];
|
||||
const iconFile = files.icon?.[0];
|
||||
const versionManifestFile = files.versionManifest?.[0];
|
||||
|
||||
if (packageFile) {
|
||||
patch.storagePath = packageFile.path;
|
||||
@@ -292,6 +334,16 @@ async function makeFilePatch(files = {}) {
|
||||
patch.iconUrl = `/uploads/icons/${encodeURIComponent(iconFile.filename)}`;
|
||||
}
|
||||
|
||||
if (versionManifestFile) {
|
||||
const versionManifest = await readJsonFile(versionManifestFile.path);
|
||||
patch.versionManifest = versionManifest;
|
||||
patch.versionManifestStoragePath = versionManifestFile.path;
|
||||
patch.versionManifestFileName = versionManifestFile.filename;
|
||||
patch.versionManifestOriginalFileName = versionManifestFile.originalname;
|
||||
patch.versionManifestSha256 = await hashFile(versionManifestFile.path);
|
||||
patch.versionManifestUploadedAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
@@ -316,7 +368,165 @@ function getBodyPatch(body, existing = {}) {
|
||||
}
|
||||
|
||||
function resolveDownloadUrl(item) {
|
||||
return item.downloadUrl || item.externalDownloadUrl || "";
|
||||
return item.downloadUrl || item.externalDownloadUrl || item.versionManifest?.download_url || item.versionManifest?.full_installer_url || "";
|
||||
}
|
||||
|
||||
function getLatestVersion(item) {
|
||||
return String(item.versionManifest?.version || item.version || "").trim();
|
||||
}
|
||||
|
||||
function normalizeVersion(value) {
|
||||
return String(value || "").trim().replace(/^v/i, "");
|
||||
}
|
||||
|
||||
function tokenizeVersion(value) {
|
||||
return normalizeVersion(value)
|
||||
.split(/[.+\-_]/)
|
||||
.filter(Boolean)
|
||||
.map((part) => (/^\d+$/.test(part) ? Number(part) : part.toLowerCase()));
|
||||
}
|
||||
|
||||
function compareVersions(currentVersion, latestVersion) {
|
||||
const current = tokenizeVersion(currentVersion);
|
||||
const latest = tokenizeVersion(latestVersion);
|
||||
const length = Math.max(current.length, latest.length);
|
||||
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const a = current[index] ?? 0;
|
||||
const b = latest[index] ?? 0;
|
||||
if (a === b) continue;
|
||||
|
||||
if (typeof a === "number" && typeof b === "number") {
|
||||
return a < b ? -1 : 1;
|
||||
}
|
||||
return String(a).localeCompare(String(b), "en", { numeric: true }) < 0 ? -1 : 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getProtectedDownloadUrl(item) {
|
||||
return resolveDownloadUrl(item) ? `/api/software/${encodeURIComponent(item.id)}/download` : "";
|
||||
}
|
||||
|
||||
function getReleaseNotes(item, manifest) {
|
||||
return manifest.release_notes || manifest.releaseNotes || manifest.changelog || item.changelog || "";
|
||||
}
|
||||
|
||||
function createStoredVersionManifest(item, sourceManifest = {}) {
|
||||
const versionManifestUrl = `/api/software/${encodeURIComponent(item.id)}/version.json`;
|
||||
const downloadUrl = getProtectedDownloadUrl(item);
|
||||
const releaseNotes = getReleaseNotes(item, sourceManifest);
|
||||
const minimumVersion = sourceManifest.min_supported_version || sourceManifest.minSupportedVersion || sourceManifest.minimumVersion || sourceManifest.minVersion || "";
|
||||
const forceUpdate = Boolean(sourceManifest.forceUpdate || sourceManifest.force_update || sourceManifest.force);
|
||||
|
||||
return {
|
||||
...sourceManifest,
|
||||
version: String(sourceManifest.version || item.version || "1.0.0").trim(),
|
||||
update_url: sourceManifest.update_url || versionManifestUrl,
|
||||
updateUrl: sourceManifest.updateUrl || sourceManifest.update_url || versionManifestUrl,
|
||||
release_notes: releaseNotes,
|
||||
releaseNotes,
|
||||
changelog: sourceManifest.changelog || releaseNotes,
|
||||
download_url: sourceManifest.download_url || downloadUrl,
|
||||
downloadUrl: sourceManifest.downloadUrl || sourceManifest.download_url || downloadUrl,
|
||||
full_installer_url: sourceManifest.full_installer_url || sourceManifest.download_url || downloadUrl,
|
||||
fullInstallerUrl: sourceManifest.fullInstallerUrl || sourceManifest.full_installer_url || sourceManifest.downloadUrl || sourceManifest.download_url || downloadUrl,
|
||||
forceUpdate,
|
||||
force_update: forceUpdate,
|
||||
minSupportedVersion: sourceManifest.minSupportedVersion || sourceManifest.min_supported_version || minimumVersion,
|
||||
min_supported_version: sourceManifest.min_supported_version || sourceManifest.minSupportedVersion || minimumVersion,
|
||||
deltaUpdates: sourceManifest.deltaUpdates || sourceManifest.delta_updates || {},
|
||||
delta_updates: sourceManifest.delta_updates || sourceManifest.deltaUpdates || {},
|
||||
};
|
||||
}
|
||||
|
||||
function applyVersionManifest(item, sourceManifest, mode = "auto") {
|
||||
const manifest = createStoredVersionManifest(item, sourceManifest || {});
|
||||
item.versionManifest = manifest;
|
||||
item.version = String(manifest.version || item.version || "1.0.0").trim();
|
||||
item.versionManifestUploadedAt = new Date().toISOString();
|
||||
item.versionManifestSource = mode;
|
||||
item.versionManifestUrl = `/api/software/${encodeURIComponent(item.id)}/version.json`;
|
||||
item.checkUpdateUrl = `/api/software/${encodeURIComponent(item.id)}/check-update`;
|
||||
return item;
|
||||
}
|
||||
|
||||
function buildVersionManifest(item) {
|
||||
const sourceManifest = item.versionManifest && typeof item.versionManifest === "object" ? item.versionManifest : {};
|
||||
const latestVersion = getLatestVersion(item);
|
||||
const downloadUrl = getProtectedDownloadUrl(item);
|
||||
const versionManifestUrl = `/api/software/${encodeURIComponent(item.id)}/version.json`;
|
||||
const releaseNotes = getReleaseNotes(item, sourceManifest);
|
||||
const minimumVersion = sourceManifest.min_supported_version || sourceManifest.minSupportedVersion || sourceManifest.minimumVersion || sourceManifest.minVersion || "";
|
||||
|
||||
return {
|
||||
...sourceManifest,
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
version: latestVersion,
|
||||
updatedAt: item.updatedAt,
|
||||
changelog: releaseNotes,
|
||||
releaseNotes,
|
||||
release_notes: releaseNotes,
|
||||
size: sourceManifest.size || item.size || "",
|
||||
sizeBytes: sourceManifest.sizeBytes ?? item.sizeBytes ?? 0,
|
||||
sha256: sourceManifest.sha256 || item.sha256 || "",
|
||||
updateUrl: versionManifestUrl,
|
||||
update_url: versionManifestUrl,
|
||||
downloadUrl,
|
||||
download_url: downloadUrl,
|
||||
fullInstallerUrl: downloadUrl,
|
||||
full_installer_url: downloadUrl,
|
||||
forceUpdate: Boolean(sourceManifest.forceUpdate || sourceManifest.force_update || sourceManifest.force),
|
||||
force_update: Boolean(sourceManifest.forceUpdate || sourceManifest.force_update || sourceManifest.force),
|
||||
minSupportedVersion: minimumVersion,
|
||||
min_supported_version: minimumVersion,
|
||||
deltaUpdates: sourceManifest.deltaUpdates || sourceManifest.delta_updates || {},
|
||||
delta_updates: sourceManifest.delta_updates || sourceManifest.deltaUpdates || {},
|
||||
checkUpdateUrl: `/api/software/${encodeURIComponent(item.id)}/check-update`,
|
||||
versionManifestUrl,
|
||||
};
|
||||
}
|
||||
|
||||
function buildUpdateCheck(item, currentVersion) {
|
||||
const manifest = buildVersionManifest(item);
|
||||
const latestVersion = manifest.version;
|
||||
const comparison = compareVersions(currentVersion, latestVersion);
|
||||
const minimumVersion = manifest.min_supported_version || manifest.minSupportedVersion || manifest.minimumVersion || manifest.minVersion || "";
|
||||
const belowMinimum = minimumVersion ? compareVersions(currentVersion, minimumVersion) < 0 : false;
|
||||
const updateAvailable = comparison < 0;
|
||||
const forceUpdate = Boolean(manifest.forceUpdate || manifest.force_update || manifest.force || belowMinimum);
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
updateAvailable,
|
||||
comparison,
|
||||
forceUpdate,
|
||||
minSupportedVersion: minimumVersion,
|
||||
min_supported_version: minimumVersion,
|
||||
belowMinimum,
|
||||
updatedAt: item.updatedAt,
|
||||
changelog: manifest.changelog || "",
|
||||
releaseNotes: manifest.releaseNotes || manifest.release_notes || "",
|
||||
release_notes: manifest.release_notes || manifest.releaseNotes || "",
|
||||
size: manifest.size || "",
|
||||
sizeBytes: manifest.sizeBytes || 0,
|
||||
sha256: manifest.sha256 || "",
|
||||
downloadUrl: manifest.downloadUrl,
|
||||
download_url: manifest.download_url,
|
||||
fullInstallerUrl: manifest.fullInstallerUrl,
|
||||
full_installer_url: manifest.full_installer_url,
|
||||
updateUrl: manifest.updateUrl,
|
||||
update_url: manifest.update_url,
|
||||
deltaUpdates: manifest.deltaUpdates || {},
|
||||
delta_updates: manifest.delta_updates || {},
|
||||
versionManifestUrl: manifest.versionManifestUrl,
|
||||
manifest,
|
||||
};
|
||||
}
|
||||
|
||||
function isPathInside(parent, child) {
|
||||
@@ -332,9 +542,28 @@ function resolvePackagePath(item) {
|
||||
return "";
|
||||
}
|
||||
|
||||
async function cleanupUploadedFiles(req) {
|
||||
const uploadedFiles = Object.values(req.files || {}).flat();
|
||||
for (const file of uploadedFiles) {
|
||||
if (!file?.path) continue;
|
||||
const filePath = path.resolve(file.path);
|
||||
if (isPathInside(uploadDir, filePath)) {
|
||||
await fsp.rm(filePath, { force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination(req, file, callback) {
|
||||
callback(null, file.fieldname === "icon" ? iconDir : packageDir);
|
||||
if (file.fieldname === "icon") {
|
||||
callback(null, iconDir);
|
||||
return;
|
||||
}
|
||||
if (file.fieldname === "versionManifest") {
|
||||
callback(null, manifestDir);
|
||||
return;
|
||||
}
|
||||
callback(null, packageDir);
|
||||
},
|
||||
filename(req, file, callback) {
|
||||
const ext = path.extname(file.originalname || "").slice(0, 24);
|
||||
@@ -348,11 +577,24 @@ const upload = multer({
|
||||
storage,
|
||||
limits: {
|
||||
fileSize: Number(process.env.MAX_UPLOAD_BYTES || 1024 * 1024 * 1024),
|
||||
files: 2,
|
||||
files: 3,
|
||||
},
|
||||
fileFilter(req, file, callback) {
|
||||
if (file.fieldname === "icon" && !file.mimetype.startsWith("image/")) {
|
||||
callback(new Error("Icon must be an image file"));
|
||||
const error = new Error("Icon must be an image file");
|
||||
error.status = 400;
|
||||
callback(error);
|
||||
return;
|
||||
}
|
||||
if (file.fieldname === "versionManifest") {
|
||||
const isJson = file.mimetype === "application/json" || file.originalname.toLowerCase().endsWith(".json");
|
||||
if (!isJson) {
|
||||
const error = new Error("version.json must be a JSON file");
|
||||
error.status = 400;
|
||||
callback(error);
|
||||
return;
|
||||
}
|
||||
callback(null, true);
|
||||
return;
|
||||
}
|
||||
callback(null, true);
|
||||
@@ -442,22 +684,86 @@ app.get("/api/software/:id/latest", async (req, res, next) => {
|
||||
res.status(404).json({ error: "Software not found" });
|
||||
return;
|
||||
}
|
||||
const manifest = buildVersionManifest(item);
|
||||
res.json({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
version: item.version,
|
||||
changelog: item.changelog,
|
||||
version: manifest.version,
|
||||
changelog: manifest.changelog,
|
||||
releaseNotes: manifest.releaseNotes,
|
||||
release_notes: manifest.release_notes,
|
||||
updatedAt: item.updatedAt,
|
||||
size: item.size,
|
||||
sizeBytes: item.sizeBytes,
|
||||
sha256: item.sha256,
|
||||
downloadUrl: `/api/software/${encodeURIComponent(item.id)}/download`,
|
||||
size: manifest.size,
|
||||
sizeBytes: manifest.sizeBytes,
|
||||
sha256: manifest.sha256,
|
||||
downloadUrl: manifest.downloadUrl,
|
||||
download_url: manifest.download_url,
|
||||
fullInstallerUrl: manifest.fullInstallerUrl,
|
||||
full_installer_url: manifest.full_installer_url,
|
||||
updateUrl: manifest.updateUrl,
|
||||
update_url: manifest.update_url,
|
||||
versionManifestUrl: manifest.versionManifestUrl,
|
||||
checkUpdateUrl: manifest.checkUpdateUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/software/:id/version.json", async (req, res, next) => {
|
||||
try {
|
||||
const { items } = await readStore();
|
||||
const item = items.find((entry) => entry.id === req.params.id && entry.active !== false);
|
||||
if (!item) {
|
||||
res.status(404).json({ error: "Software not found" });
|
||||
return;
|
||||
}
|
||||
res.json(buildVersionManifest(item));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/software/:id/check-update", async (req, res, next) => {
|
||||
try {
|
||||
const currentVersion = String(req.query.version || req.query.currentVersion || "").trim();
|
||||
if (!currentVersion) {
|
||||
res.status(400).json({ error: "Current version is required. Use ?version=1.0.0" });
|
||||
return;
|
||||
}
|
||||
|
||||
const { items } = await readStore();
|
||||
const item = items.find((entry) => entry.id === req.params.id && entry.active !== false);
|
||||
if (!item) {
|
||||
res.status(404).json({ error: "Software not found" });
|
||||
return;
|
||||
}
|
||||
res.json(buildUpdateCheck(item, currentVersion));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/software/:id/check-update", async (req, res, next) => {
|
||||
try {
|
||||
const currentVersion = String(req.body.version || req.body.currentVersion || "").trim();
|
||||
if (!currentVersion) {
|
||||
res.status(400).json({ error: "Current version is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const { items } = await readStore();
|
||||
const item = items.find((entry) => entry.id === req.params.id && entry.active !== false);
|
||||
if (!item) {
|
||||
res.status(404).json({ error: "Software not found" });
|
||||
return;
|
||||
}
|
||||
res.json(buildUpdateCheck(item, currentVersion));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/software/:id/download", requirePlayer, async (req, res, next) => {
|
||||
try {
|
||||
const store = await readStore();
|
||||
@@ -493,7 +799,7 @@ app.get("/api/software/:id/download", requirePlayer, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/admin/software", requireAdmin, async (req, res, next) => {
|
||||
app.get("/api/admin/software", requirePlayer, requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const { items } = await readStore();
|
||||
res.json({ items: sortSoftware(items).map((item) => publicItem(item, true)) });
|
||||
@@ -502,7 +808,7 @@ app.get("/api/admin/software", requireAdmin, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/admin/software", requireAdmin, upload.fields([{ name: "package", maxCount: 1 }, { name: "icon", maxCount: 1 }]), async (req, res, next) => {
|
||||
app.post("/api/admin/software", requirePlayer, requireAdmin, upload.fields([{ name: "package", maxCount: 1 }, { name: "icon", maxCount: 1 }, { name: "versionManifest", maxCount: 1 }]), async (req, res, next) => {
|
||||
try {
|
||||
const bodyPatch = getBodyPatch(req.body);
|
||||
if (!bodyPatch.name) {
|
||||
@@ -518,6 +824,11 @@ app.post("/api/admin/software", requireAdmin, upload.fields([{ name: "package",
|
||||
}
|
||||
|
||||
const filePatch = await makeFilePatch(req.files);
|
||||
const contentManifest = parseVersionManifestContent(req.body.versionManifestContent);
|
||||
const explicitManifest = filePatch.versionManifest || contentManifest;
|
||||
if (explicitManifest?.version) {
|
||||
bodyPatch.version = String(explicitManifest.version).trim();
|
||||
}
|
||||
const now = new Date().toISOString();
|
||||
const externalDownloadUrl = bodyPatch.externalDownloadUrl;
|
||||
const item = {
|
||||
@@ -530,6 +841,14 @@ app.post("/api/admin/software", requireAdmin, upload.fields([{ name: "package",
|
||||
externalDownloadUrl: filePatch.downloadUrl ? "" : externalDownloadUrl,
|
||||
};
|
||||
|
||||
if (explicitManifest) {
|
||||
applyVersionManifest(item, explicitManifest, filePatch.versionManifest ? "file" : "manual");
|
||||
} else if (resolveDownloadUrl(item)) {
|
||||
applyVersionManifest(item, {}, "auto");
|
||||
} else {
|
||||
item.versionManifestUrl = `/api/software/${encodeURIComponent(item.id)}/version.json`;
|
||||
item.checkUpdateUrl = `/api/software/${encodeURIComponent(item.id)}/check-update`;
|
||||
}
|
||||
store.items.push(item);
|
||||
await writeStore(store);
|
||||
res.status(201).json({ item: publicItem(item, true) });
|
||||
@@ -538,7 +857,7 @@ app.post("/api/admin/software", requireAdmin, upload.fields([{ name: "package",
|
||||
}
|
||||
});
|
||||
|
||||
app.put("/api/admin/software/:id", requireAdmin, upload.fields([{ name: "package", maxCount: 1 }, { name: "icon", maxCount: 1 }]), async (req, res, next) => {
|
||||
app.put("/api/admin/software/:id", requirePlayer, requireAdmin, upload.fields([{ name: "package", maxCount: 1 }, { name: "icon", maxCount: 1 }, { name: "versionManifest", maxCount: 1 }]), async (req, res, next) => {
|
||||
try {
|
||||
const store = await readStore();
|
||||
const index = store.items.findIndex((item) => item.id === req.params.id);
|
||||
@@ -555,6 +874,11 @@ app.put("/api/admin/software/:id", requireAdmin, upload.fields([{ name: "package
|
||||
}
|
||||
|
||||
const filePatch = await makeFilePatch(req.files);
|
||||
const contentManifest = parseVersionManifestContent(req.body.versionManifestContent);
|
||||
const explicitManifest = filePatch.versionManifest || contentManifest;
|
||||
if (explicitManifest?.version) {
|
||||
bodyPatch.version = String(explicitManifest.version).trim();
|
||||
}
|
||||
const downloadUrl = filePatch.downloadUrl || bodyPatch.externalDownloadUrl || existing.downloadUrl || "";
|
||||
const item = {
|
||||
...existing,
|
||||
@@ -563,6 +887,17 @@ app.put("/api/admin/software/:id", requireAdmin, upload.fields([{ name: "package
|
||||
downloadUrl,
|
||||
externalDownloadUrl: filePatch.downloadUrl ? "" : bodyPatch.externalDownloadUrl,
|
||||
};
|
||||
if (explicitManifest) {
|
||||
applyVersionManifest(item, explicitManifest, filePatch.versionManifest ? "file" : "manual");
|
||||
} else if (filePatch.downloadUrl || bodyPatch.externalDownloadUrl || !item.versionManifest) {
|
||||
const autoSource = item.versionManifest
|
||||
? { delta_updates: item.versionManifest.delta_updates || item.versionManifest.deltaUpdates || {} }
|
||||
: {};
|
||||
applyVersionManifest(item, autoSource, "auto");
|
||||
} else {
|
||||
item.versionManifestUrl = `/api/software/${encodeURIComponent(item.id)}/version.json`;
|
||||
item.checkUpdateUrl = `/api/software/${encodeURIComponent(item.id)}/check-update`;
|
||||
}
|
||||
|
||||
store.items[index] = item;
|
||||
await writeStore(store);
|
||||
@@ -572,7 +907,7 @@ app.put("/api/admin/software/:id", requireAdmin, upload.fields([{ name: "package
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/admin/software/:id", requireAdmin, async (req, res, next) => {
|
||||
app.delete("/api/admin/software/:id", requirePlayer, requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const store = await readStore();
|
||||
const initialLength = store.items.length;
|
||||
@@ -591,17 +926,18 @@ app.delete("/api/admin/software/:id", requireAdmin, async (req, res, next) => {
|
||||
app.get("/software", (req, res) => res.redirect(301, "/software.html"));
|
||||
app.get("/software/", (req, res) => res.sendFile(path.join(projectRoot, "software.html")));
|
||||
app.get("/login", (req, res) => res.redirect(301, "/login.html"));
|
||||
app.get("/admin/software", (req, res) => res.sendFile(path.join(projectRoot, "admin", "software.html")));
|
||||
app.get("/admin/software", requirePlayer, (req, res) => res.sendFile(path.join(projectRoot, "admin", "software.html")));
|
||||
app.get("/", (req, res) => res.sendFile(path.join(projectRoot, "index.html")));
|
||||
app.get("/index.html", (req, res) => res.sendFile(path.join(projectRoot, "index.html")));
|
||||
app.get("/software.html", (req, res) => res.sendFile(path.join(projectRoot, "software.html")));
|
||||
app.get("/login.html", (req, res) => res.sendFile(path.join(projectRoot, "login.html")));
|
||||
app.get("/admin/software.html", (req, res) => res.sendFile(path.join(projectRoot, "admin", "software.html")));
|
||||
app.get("/admin/software.html", requirePlayer, (req, res) => res.sendFile(path.join(projectRoot, "admin", "software.html")));
|
||||
app.use("/assets", express.static(path.join(projectRoot, "assets"), { fallthrough: false }));
|
||||
|
||||
app.use((error, req, res, next) => {
|
||||
app.use(async (error, req, res, next) => {
|
||||
await cleanupUploadedFiles(req);
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message || "Internal server error" });
|
||||
res.status(error.status || 500).json({ error: error.message || "Internal server error" });
|
||||
});
|
||||
|
||||
await ensureStorage();
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user