Add rate limit using .env and fix disappearing image grid after opening settings

This commit is contained in:
2026-03-13 17:02:40 +01:00
parent 07d0dcd41f
commit ddfc069047
4 changed files with 88 additions and 11 deletions

21
main.js
View File

@@ -4,13 +4,32 @@ import { randomUUID } from "crypto";
import axios from "axios"; import axios from "axios";
import archiver from "archiver"; import archiver from "archiver";
import mime from "mime"; import mime from "mime";
import { rateLimit } from "express-rate-limit";
import "dotenv/config";
const app = express(); const app = express();
const PORT = 3000; const PORT = 3000;
const useRateLimit = process.env.USE_RATE_LIMIT === "true";
const getImagesLimiter = rateLimit({
windowMs: 60 * 1000, // 1 min
limit: 100,
standardHeaders: "draft-8",
legacyHeaders: false,
});
const downloadLimiter = rateLimit({
windowMs: 60 * 1000, // 1 min
limit: 5,
standardHeaders: "draft-8",
legacyHeaders: false,
});
let cachedImagesUrls = {}; let cachedImagesUrls = {};
app.use(express.static("public")); app.use(express.static("public"));
if (useRateLimit) {
app.use("/api/getImagesURL", getImagesLimiter);
app.use("/api/downloadImages", downloadLimiter);
}
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Server launched on http://localhost:${PORT}`); console.log(`Server launched on http://localhost:${PORT}`);
@@ -95,7 +114,7 @@ app.get("/api/downloadImages", async (req, res) => {
for (let i = 0; i < imagesUrls.length; i++) { for (let i = 0; i < imagesUrls.length; i++) {
const url = imagesUrls[i]; const url = imagesUrls[i];
try { try {
const response = await axios.get(url, { responseType: "arraybuffer", timeout: 5000 }); const response = await axios.get(url, { responseType: "stream", timeout: 5000 });
const contentType = response.headers["content-type"]; const contentType = response.headers["content-type"];
const extension = mime.getExtension(contentType) || url.split(".").pop(); const extension = mime.getExtension(contentType) || url.split(".").pop();

41
package-lock.json generated
View File

@@ -12,7 +12,9 @@
"archiver": "^7.0.1", "archiver": "^7.0.1",
"axios": "^1.13.6", "axios": "^1.13.6",
"cheerio": "^1.2.0", "cheerio": "^1.2.0",
"dotenv": "^17.3.1",
"express": "^5.2.1", "express": "^5.2.1",
"express-rate-limit": "^8.3.1",
"mime": "^4.1.0" "mime": "^4.1.0"
} }
}, },
@@ -679,6 +681,18 @@
"url": "https://github.com/fb55/domutils?sponsor=1" "url": "https://github.com/fb55/domutils?sponsor=1"
} }
}, },
"node_modules/dotenv": {
"version": "17.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
"integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -887,6 +901,24 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-rate-limit": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
"integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==",
"license": "MIT",
"dependencies": {
"ip-address": "10.1.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": ">= 4.11"
}
},
"node_modules/fast-fifo": { "node_modules/fast-fifo": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
@@ -1222,6 +1254,15 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",

View File

@@ -13,7 +13,9 @@
"archiver": "^7.0.1", "archiver": "^7.0.1",
"axios": "^1.13.6", "axios": "^1.13.6",
"cheerio": "^1.2.0", "cheerio": "^1.2.0",
"dotenv": "^17.3.1",
"express": "^5.2.1", "express": "^5.2.1",
"express-rate-limit": "^8.3.1",
"mime": "^4.1.0" "mime": "^4.1.0"
} }
} }

View File

@@ -2,7 +2,8 @@ const starsNumber = 1000;
const view = document.getElementById("view"); const view = document.getElementById("view");
const homeTemplate = document.getElementById("home-template"); const homeTemplate = document.getElementById("home-template");
const settingsTemplate = document.getElementById("settings-template"); const settingsTemplate = document.getElementById("settings-template");
let imagesUrls = []; let cachedUrls = [];
let cachedQuery = "";
let uuid; let uuid;
let imagesProvider = "Bing"; let imagesProvider = "Bing";
let imagesOffset = 1; let imagesOffset = 1;
@@ -90,10 +91,14 @@ function navigate(page) {
case "home": case "home":
view.appendChild(homeTemplate.content.cloneNode(true)); view.appendChild(homeTemplate.content.cloneNode(true));
const searchForm = document.getElementById("search-form"); const searchForm = document.getElementById("search-form");
const searchInput = document.getElementById("search-input");
searchForm.addEventListener("submit", function (event) { searchForm.addEventListener("submit", function (event) {
event.preventDefault(); event.preventDefault();
}); });
fillImagesGrid();
searchInput.value = cachedQuery;
break; break;
case "settings": case "settings":
view.appendChild(settingsTemplate.content.cloneNode(true)); view.appendChild(settingsTemplate.content.cloneNode(true));
@@ -131,7 +136,7 @@ async function getImagesURL(query, offset, count, smart) {
const data = await response.json(); const data = await response.json();
uuid = data.uuid; uuid = data.uuid;
return data.urls; cachedUrls = data.urls;
} }
async function downloadImages(uuid) { async function downloadImages(uuid) {
@@ -154,22 +159,17 @@ async function downloadImages(uuid) {
} }
async function search() { async function search() {
const imageTemplate = document.getElementById("image-template");
const imagesDiv = document.getElementById("images-div"); const imagesDiv = document.getElementById("images-div");
const loaderDiv = document.getElementById("loader-div"); const loaderDiv = document.getElementById("loader-div");
const searchInput = document.getElementById("search-input"); const searchInput = document.getElementById("search-input");
cachedQuery = searchInput.value;
imagesDiv.replaceChildren(); imagesDiv.replaceChildren();
loaderDiv.classList.toggle("show"); loaderDiv.classList.toggle("show");
const urls = await getImagesURL(searchInput.value, imagesOffset, maxImages, smartMode); await getImagesURL(cachedQuery, imagesOffset, maxImages, smartMode);
loaderDiv.classList.toggle("show"); loaderDiv.classList.toggle("show");
for (const url of urls) { fillImagesGrid();
imagesUrls.push(url);
const imageTemplateCopy = imageTemplate.content.cloneNode(true);
imageTemplateCopy.getElementById("image").src = url;
imagesDiv.appendChild(imageTemplateCopy);
}
} }
async function download() { async function download() {
@@ -210,3 +210,18 @@ function toggleSelectedListButton(ListMenuId, buttonId) {
listButton.classList.toggle("selected"); listButton.classList.toggle("selected");
} }
} }
function fillImagesGrid() {
if (!cachedUrls) {
return;
}
const imageTemplate = document.getElementById("image-template");
const imagesDiv = document.getElementById("images-div");
for (const url of cachedUrls) {
const imageTemplateCopy = imageTemplate.content.cloneNode(true);
imageTemplateCopy.getElementById("image").src = url;
imagesDiv.appendChild(imageTemplateCopy);
}
}