diff --git a/main.js b/main.js index 17f2a9d..d5b1b41 100644 --- a/main.js +++ b/main.js @@ -4,13 +4,32 @@ import { randomUUID } from "crypto"; import axios from "axios"; import archiver from "archiver"; import mime from "mime"; +import { rateLimit } from "express-rate-limit"; +import "dotenv/config"; const app = express(); 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 = {}; app.use(express.static("public")); +if (useRateLimit) { + app.use("/api/getImagesURL", getImagesLimiter); + app.use("/api/downloadImages", downloadLimiter); +} app.listen(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++) { const url = imagesUrls[i]; 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 extension = mime.getExtension(contentType) || url.split(".").pop(); diff --git a/package-lock.json b/package-lock.json index 1eb9619..1682fc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,9 @@ "archiver": "^7.0.1", "axios": "^1.13.6", "cheerio": "^1.2.0", + "dotenv": "^17.3.1", "express": "^5.2.1", + "express-rate-limit": "^8.3.1", "mime": "^4.1.0" } }, @@ -679,6 +681,18 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -887,6 +901,24 @@ "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": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -1222,6 +1254,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "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": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/package.json b/package.json index f5a0d28..5859016 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "archiver": "^7.0.1", "axios": "^1.13.6", "cheerio": "^1.2.0", + "dotenv": "^17.3.1", "express": "^5.2.1", + "express-rate-limit": "^8.3.1", "mime": "^4.1.0" } } diff --git a/public/script.js b/public/script.js index 08c0425..1ec5b5f 100644 --- a/public/script.js +++ b/public/script.js @@ -2,7 +2,8 @@ const starsNumber = 1000; const view = document.getElementById("view"); const homeTemplate = document.getElementById("home-template"); const settingsTemplate = document.getElementById("settings-template"); -let imagesUrls = []; +let cachedUrls = []; +let cachedQuery = ""; let uuid; let imagesProvider = "Bing"; let imagesOffset = 1; @@ -90,10 +91,14 @@ function navigate(page) { case "home": view.appendChild(homeTemplate.content.cloneNode(true)); const searchForm = document.getElementById("search-form"); + const searchInput = document.getElementById("search-input"); searchForm.addEventListener("submit", function (event) { event.preventDefault(); }); + + fillImagesGrid(); + searchInput.value = cachedQuery; break; case "settings": view.appendChild(settingsTemplate.content.cloneNode(true)); @@ -131,7 +136,7 @@ async function getImagesURL(query, offset, count, smart) { const data = await response.json(); uuid = data.uuid; - return data.urls; + cachedUrls = data.urls; } async function downloadImages(uuid) { @@ -154,22 +159,17 @@ async function downloadImages(uuid) { } async function search() { - const imageTemplate = document.getElementById("image-template"); const imagesDiv = document.getElementById("images-div"); const loaderDiv = document.getElementById("loader-div"); const searchInput = document.getElementById("search-input"); + cachedQuery = searchInput.value; imagesDiv.replaceChildren(); loaderDiv.classList.toggle("show"); - const urls = await getImagesURL(searchInput.value, imagesOffset, maxImages, smartMode); + await getImagesURL(cachedQuery, imagesOffset, maxImages, smartMode); loaderDiv.classList.toggle("show"); - for (const url of urls) { - imagesUrls.push(url); - const imageTemplateCopy = imageTemplate.content.cloneNode(true); - imageTemplateCopy.getElementById("image").src = url; - imagesDiv.appendChild(imageTemplateCopy); - } + fillImagesGrid(); } async function download() { @@ -210,3 +210,18 @@ function toggleSelectedListButton(ListMenuId, buttonId) { 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); + } +}