initial changes

This commit is contained in:
2025-03-03 08:28:30 -06:00
parent 03b7b9fca2
commit 486d7e3a6f
20 changed files with 457 additions and 2 deletions

127
src/api.js Normal file
View File

@@ -0,0 +1,127 @@
import {
DeleteObjectCommand,
GetObjectCommand,
ListObjectsCommand,
S3Client,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const BUCKET = "repertory";
const oldItems = [];
const s3 = new S3Client({
region: "any",
endpoint: "https://gateway.storjshare.io",
forcePathStyle: true,
credentials: {
accessKeyId: process.env.R_AWS_KEY,
secretAccessKey: process.env.R_AWS_SECRET,
},
});
const cleanOldItems = async () => {
console.log("cleanOldItems", oldItems.length);
while (oldItems.length > 0) {
const key = oldItems.pop();
await s3.send(
new DeleteObjectCommand({
Bucket: BUCKET,
Key: key,
}),
);
console.log(key);
}
};
const createDownloadLink = async (key) => {
let filename = key.split("/");
filename = filename[filename.length - 1];
return await getSignedUrl(
s3,
new GetObjectCommand({
Bucket: BUCKET,
Key: key,
ResponseContentDisposition: `attachment; filename="${filename}"`,
}),
{ expiresIn: 3600 },
);
};
const getAllFiles = async () => {
const data = await s3.send(
new ListObjectsCommand({
Bucket: BUCKET,
Delimiter: "/",
}),
);
console.log(data);
};
const getBucketFiles = async (folderName) => {
try {
folderName = folderName.toLowerCase();
const folderKey = encodeURIComponent(folderName) + "/";
const data = await s3.send(
new ListObjectsCommand({
Bucket: BUCKET,
Prefix: folderKey,
}),
);
const ret = data.Contents.filter((item) => item.Key !== folderKey)
.map((item) => {
return {
date: item.LastModified.toLocaleDateString("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}).replace(/,/g, ""),
sort: item.LastModified.getTime(),
name: item.Key.replace(folderKey, ""),
key: item.Key,
};
})
.sort((a, b) => {
return a.sort > b.sort ? -1 : a.sort < b.sort ? 1 : 0;
});
const itemCount = {};
const filteredItems = ret
.filter((item) => item.name.endsWith(".tar.gz"))
.filter((item) => {
if (folderName === "nightly") {
const parts = item.name.split("_");
const groupId = `${parts[0]}_${parts[3]}_${parts[4]}`;
itemCount[groupId] = itemCount[groupId] || 0;
if (++itemCount[groupId] <= 3) {
return true;
}
if (!oldItems.find((key) => key === item.key)) {
oldItems.push(item.key);
oldItems.push(item.key + ".sha256");
oldItems.push(item.key + ".sig");
}
return false;
}
return true;
});
const totalCount = filteredItems.length * 3;
for (let i = 0; i < totalCount && i < filteredItems.length; i += 3) {
let item = ret.filter(
(item) => item.name === filteredItems[i].name + ".sha256",
);
filteredItems.splice(i + 1, 0, ...item);
item = ret.filter((item) => item.name === filteredItems[i].name + ".sig");
filteredItems.splice(i + 2, 0, ...item);
}
return filteredItems;
} catch (err) {
console.log(err);
}
return [];
};
export { cleanOldItems, createDownloadLink, getBucketFiles };

49
src/app.js Normal file
View File

@@ -0,0 +1,49 @@
import createHttpError from "http-errors";
import express from "express";
import path from "path";
import cookieParser from "cookie-parser";
import logger from "morgan";
import indexRouter from "./routes/index";
if (!process.env.R_AWS_KEY) {
console.log("FATAL: 'R_AWS_KEY' environment variable is not set");
process.exit(-1);
}
if (!process.env.R_AWS_SECRET) {
console.log("FATAL: 'R_AWS_SECRET' environment variable is not set");
process.exit(-2);
}
const app = express().set("env", process.env.NODE_ENV || "production");
const errorHandler = (err, res) => {
res.locals.message = err.message || "unknown error";
res.locals.error = app.get("env") === "development" ? err : {};
res.render("errors", {
message: "An error occurred",
error: { status: err.status || 500 },
});
};
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use("/", indexRouter);
app.use((err, req, res, next) => {
errorHandler(err, res);
});
app.use((_, res) => {
errorHandler(createHttpError(404), res);
});
module.exports = app;

6
src/mixins/download.pug Normal file
View File

@@ -0,0 +1,6 @@
mixin download(file)
.download
|[#{file.date}]
|
a(href='/'+`download?key=${file.key}`) #{file.name}

3
src/mixins/stage.pug Normal file
View File

@@ -0,0 +1,3 @@
mixin stage(name)
.stage
a(href='/'+`stage?name=${name}`) #{name}

View File

@@ -0,0 +1,29 @@
body {
padding: 0;
margin: 10px;
font: 16px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
display: inline-block;
color: #00B7FF;
font-size: 18px;
margin-bottom: 8px;
}
.download {
width: 100%;
}
.stage {
text-transform: capitalize;
}
.public_key > textarea {
height: 415px;
width: 100%;
border: none;
resize: none;
font-family: monospace;
outline: 0;
}

82
src/routes/index.js Normal file
View File

@@ -0,0 +1,82 @@
import express from "express";
import { cleanOldItems, createDownloadLink, getBucketFiles } from "../api";
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqXedleDOugdk9sBpgFOA
0+MogIbBF7+iXIIHv8CRBbrrf8nxLSgQvbHQIP0EklebDgLZRgyGI3SSQYj7D957
uNf1//dpkELNzfuezgAyFer9+iH4Svq46HADp5k+ugaK0mMDZM7OLOgo7415/+z4
NIQopv8prMFdxkShr4e4dpR+S6LYMYMVjsi1gnYWaZJMWgzeZouXFSscS1/XDXSE
vr1Jfqme+RmB4Q2QqGcDrY2ijumCJYJzQqlwG6liJ4FNg0U3POTCQDhQmuUoEJe0
/dyiWlo48WQbBu6gUDHbTCCUSZPs2Lc9l65MqOCpX76+VXPYetZgqpMF4GVzb2y9
kETxFNpiMYBlOBZk0I1G33wqVmw46MI5IZMQ2z2F8Mzt1hByUNTgup2IQELCv1a5
a2ACs2TBRuAy1REeHhjLgiA/MpoGX7TpoHCGyo8jBChJVpP9ZHltKoChwDC+bIyx
rgYH3jYDkl2FFuAUJ8zAZl8U1kjqZb9HGq9ootMk34Dbo3IVkc2azB2orEP9F8QV
KxvZZDA9FAFEthSiNf5soJ6mZGLi0es5EWPoKMUEd9tG5bP980DySAWSSRK0AOfE
QShT/z7oG79Orxyomwrb8ZJCi7wEfcCuK1NWgqLVUgXhpi2J9WYS6DAbF3Oh3Hhl
DYSHlcfFBteqNDlR2uFInIECAwEAAQ==
-----END PUBLIC KEY-----`;
const router = express.Router();
const stages = ["release", "nightly", "alpha", "beta", "RC"];
router.get("/", (_, res, next) => {
try {
res.render("index", {
title: "Fifthgrid Builds",
stages,
});
} catch (err) {
next(err);
}
});
router.get("/download", async (req, res, next) => {
try {
res.redirect(await createDownloadLink(req.query.key.toString()));
} catch (err) {
next(err);
}
});
router.get("/stage", async (req, res, next) => {
try {
const name = req.query.name.toString();
if (!stages.includes(name)) {
res.render("errors", {
message: "An error occurred",
error: { status: 404 },
});
return;
}
const files = await getBucketFiles(name);
res.render("stage", {
title: "Fifthgrid Builds",
name: name,
public_key: PUBLIC_KEY,
files: files,
});
} catch (err) {
next(err);
}
});
setInterval(
async () => {
try {
for (const stage of stages) {
try {
await getBucketFiles(stage);
} catch (e) {
console.log(e);
}
}
await cleanOldItems();
} catch (e) {
console.log(e);
}
},
15 * 60 * 1000,
);
export default router;

6
src/views/errors.pug Normal file
View File

@@ -0,0 +1,6 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}

9
src/views/index.pug Normal file
View File

@@ -0,0 +1,9 @@
extends layout
include ../mixins/stage.pug
block content
h1= title
h3= "Currently only 'nightly' and 'rc' builds are available"
each name in stages
+stage(name)

7
src/views/layout.pug Normal file
View File

@@ -0,0 +1,7 @@
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content

15
src/views/stage.pug Normal file
View File

@@ -0,0 +1,15 @@
extends layout
include ../mixins/download.pug
block content
h1= title + ' [' + name + ']'
h2= 'Public Key'
.public_key
textarea(readonly) #{public_key}
h2= 'Available Downloads'
each file, idx in files
if idx % 3 == 0
hr
+download(file)
hr