initial changes
This commit is contained in:
parent
03b7b9fca2
commit
486d7e3a6f
13
.babelrc
Normal file
13
.babelrc
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/env", {
|
||||
"targets": {
|
||||
"node": "current"
|
||||
}
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
]
|
||||
}
|
1
.cspell/words.txt
Normal file
1
.cspell/words.txt
Normal file
@ -0,0 +1 @@
|
||||
fifthgrid
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.git/
|
||||
dist/
|
||||
node_modules/
|
||||
package-lock.json
|
||||
bin/
|
||||
run.sh
|
20
.jenkins_builds
Normal file
20
.jenkins_builds
Normal file
@ -0,0 +1,20 @@
|
||||
#!groovy
|
||||
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('build') {
|
||||
steps {
|
||||
nodejs(nodeJSInstallationName: 'current') {
|
||||
sh 'npm i'
|
||||
sh 'npm run pkg'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
.vim/coc-settings.json
Normal file
5
.vim/coc-settings.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"presigner"
|
||||
]
|
||||
}
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Public
|
||||
Copyright (c) <2022> <Scott E. Graves>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
# fifthgrid_browser
|
||||
|
||||
Server to host `fifthgrid` application binaries
|
||||
Server to host `fifthgrid` application binaries
|
||||
|
16
cspell.json
Normal file
16
cspell.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
|
||||
"version": "0.2",
|
||||
"dictionaries": ["workspace-words", "user-words"],
|
||||
"dictionaryDefinitions": [{
|
||||
"name": "workspace-words",
|
||||
"path": "./.cspell/words.txt",
|
||||
"addWords": true
|
||||
},
|
||||
{
|
||||
"name": "user-words",
|
||||
"path": "C:\\.desktop\\.cspell\\user_words.txt",
|
||||
"addWords": true
|
||||
}
|
||||
]
|
||||
}
|
14
developer.pub
Normal file
14
developer.pub
Normal file
@ -0,0 +1,14 @@
|
||||
-----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-----
|
47
package.json
Normal file
47
package.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "fifthgrid-browser",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"bin": "dist/bin/www",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "rimraf dist && babel src --out-dir dist --copy-files",
|
||||
"pkg": "npm run build && pkg --out-path bin/ .",
|
||||
"start": "npm run build && node dist/bin/www",
|
||||
"start:dev": "cross-env NODE_ENV=development nodemon --exec ./node_modules/.bin/babel-node src/bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cognito-identity": "^3.53.0",
|
||||
"@aws-sdk/client-s3": "^3.53.1",
|
||||
"@aws-sdk/s3-request-presigner": "^3.53.1",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"debug": "~2.6.9",
|
||||
"express": "~4.16.1",
|
||||
"http-errors": "~1.6.3",
|
||||
"morgan": "~1.9.1",
|
||||
"pug": "3.0.2",
|
||||
"request": "^2.88.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.17.6",
|
||||
"@babel/core": "^7.17.5",
|
||||
"@babel/node": "^7.16.8",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"cross-env": "^7.0.3",
|
||||
"nodemon": "^2.0.15",
|
||||
"pkg": "^5.8.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"semgrep": "^0.0.1"
|
||||
},
|
||||
"pkg": {
|
||||
"assets": [
|
||||
"node_modules/**/*",
|
||||
"dist/**/*"
|
||||
],
|
||||
"targets": [
|
||||
"node18-linux"
|
||||
]
|
||||
}
|
||||
}
|
127
src/api.js
Normal file
127
src/api.js
Normal 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
49
src/app.js
Normal 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
6
src/mixins/download.pug
Normal 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
3
src/mixins/stage.pug
Normal file
@ -0,0 +1,3 @@
|
||||
mixin stage(name)
|
||||
.stage
|
||||
a(href='/'+`stage?name=${name}`) #{name}
|
29
src/public/stylesheets/style.css
Normal file
29
src/public/stylesheets/style.css
Normal 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
82
src/routes/index.js
Normal 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
6
src/views/errors.pug
Normal file
@ -0,0 +1,6 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
h1= message
|
||||
h2= error.status
|
||||
pre #{error.stack}
|
9
src/views/index.pug
Normal file
9
src/views/index.pug
Normal 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
7
src/views/layout.pug
Normal 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
15
src/views/stage.pug
Normal 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
|
Loading…
x
Reference in New Issue
Block a user