commit 8e5c45a7b67a73c8303998ff77050f8c77615710 Author: Fabio Date: Tue Dec 23 13:06:25 2025 +0100 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..42d826c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +npm-debug.log +Dockerfile +docker-compose.yml +.git +.git.gitignore +.env +dist +sites +server.js diff --git a/.env b/.env new file mode 100644 index 0000000..e61e342 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +TYPE=http +HOST=192.168.1.3 +PORT=3600 +URL=https://mys.patachina2.casacam.net +# SITES=composerize,composeverter,decomposerize diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45437da --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +composerize/ +composeverter/ +decomposerize/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5caa863 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,86 @@ +# -------- +# STAGE: base runtime con Node e strumenti minimi +# -------- +FROM node:20-bookworm-slim AS runtime-base + +# Impostazioni ambiente e sicurezza build +ENV DEBIAN_FRONTEND=noninteractive \ + PIP_NO_CACHE_DIR=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + NODE_ENV=production + +# Aggiorna e installa Python + strumenti +# Nota: build-essential utile per pacchetti Python nativi (puoi rimuoverlo se non necessario) +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 python3-venv python3-pip \ + ca-certificates curl tini \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Workdir applicazione +WORKDIR /usr/src/app + +# Crea utente non-root e cartelle +#RUN useradd -m -u 10001 appuser \ +# && mkdir -p /usr/src/app \ +# && chown -R appuser:appuser /usr/src/app + +# -------- +# STAGE: dipendenze Node +# -------- +FROM runtime-base AS node-deps + +WORKDIR /usr/src/app + +#copia tutto +COPY . . +COPY downloadsite-docker.sh downloadsite.sh +# Installa packages e dipendenze +RUN npm ci --only=production + +# -------- +# STAGE: dipendenze Python (venv) +# -------- +FROM node-deps AS python-deps + +WORKDIR /usr/src/app + +# crea venv +RUN python3 -m venv /opt/pyenv \ + && /opt/pyenv/bin/pip install --upgrade pip \ + && /opt/pyenv/bin/pip install -r requirements.txt + +# -------- +# STAGE: final image +# -------- +FROM python-deps AS final + +# Riduci ulteriormente l'immagine togliendo strumenti di build se li avevi installati +# (In questo esempio li teniamo per eventuali run-time native libs; opzionale rimuoverli con apt-get purge) +WORKDIR /usr/src/app + + + +# Imposta PATH per usare il venv e crea symlink "python3" puntato al venv +ENV PATH="/opt/pyenv/bin:${PATH}" +ENV PATH="/usr/src/app:${PATH}" +# Symlink esplicito utile se il tuo codice chiama "python3" hard-coded +RUN ln -sf /opt/pyenv/bin/python /usr/local/bin/python3 + +# Sicurezza: esegui come utente non-root +#USER appuser + +# Espone la porta dell'Express server +EXPOSE 3600 + +# Healthcheck semplice sulla root +#HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ +# CMD curl -fsS http://localhost:3000/ || exit 1 + +# Usa tini come init per gestire segnali e orphan processes +ENTRYPOINT ["/usr/bin/tini", "--"] + + +# Avvio +CMD ["node", "server_docker.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3727d1 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Creare un server per più website statici + +per questo esempio scaricheremo i siti dal web usando + +(https://forgit.patachina.it/Fabio/website-downloader.git) + +1. creare il folder principale es: dock clonando la git + +```sh +git clone https://forgit.patachina.it/Fabio/multi_static_website.git dock +``` + +2. scaricare i vari siti in directory differenti all'interno di dock + +``` +cd dock +downloadsite.sh https://www.decomposerize.com/ decomposerize +downloadsite.sh https://www.composerize.com/ composerize +downloadsite.sh https://www.composeverter.com/ composeverter +``` + +3. installare i packages per il server npm + + npm install + +4. inserire i parametri del server + + http o https + IP + porta + +5. inserire le directory separate da , + + SITES= + +6. il file diventa + +```sh +TYPE=http +HOST=192.168.1.3 +PORT=12000 +SITES=composerize,composeverter,decomposerize +``` + +7. avviare il server + + node server.js diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a70ce88 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + app: + image: sites:latest + container_name: sites + restart: unless-stopped + ports: + - 3600:3000 + volumes: + - /home/nvme/dockerdata/sites:/usr/src/app/sites + environment: + NODE_ENV: production + PORT: 3000 + HOST: 0.0.0.0 + TYPE: http + URL: https://mys.patachina2.casacam.net diff --git a/downloadsite-docker.sh b/downloadsite-docker.sh new file mode 100755 index 0000000..a7dbdc2 --- /dev/null +++ b/downloadsite-docker.sh @@ -0,0 +1,6 @@ +#!/bin/bash +python website-downloader.py \ + --url $1 \ + --destination $2 \ + --max-pages 100 \ + --threads 8 diff --git a/downloadsite.sh b/downloadsite.sh new file mode 100755 index 0000000..b1ccbf5 --- /dev/null +++ b/downloadsite.sh @@ -0,0 +1,7 @@ +#!/bin/bash +source /usr/local/python/website-downloader/.venv/bin/activate +python /usr/local/python/website-downloader/website-downloader.py \ + --url $1 \ + --destination $2 \ + --max-pages 100 \ + --threads 8 diff --git a/generateSitesJson.js b/generateSitesJson.js new file mode 100644 index 0000000..fcf80f1 --- /dev/null +++ b/generateSitesJson.js @@ -0,0 +1,163 @@ +// Assicurati di avere "type": "module" nel package.json +// oppure salva il file come .mjs + +import fs from "fs"; +import path from "path"; + + +const root = "./home/sites"; // cartella principale +const outputFile = "./home/sites.json"; // file di output +const iconsDir = "./home/icons"; // cartella dove salvare le icone +const baseDir = "./home"; + + + +/** + * Cancella un'icona dato il path relativo (es. "icons/composerize.ico") + * @param {string} iconPath - Percorso relativo dell'icona rispetto a baseDir + */ +function deleteIconSync(iconPath) { + try { + const fullPath = path.join(baseDir, iconPath); + fs.unlinkSync(fullPath); + console.log(`Cancellato: ${fullPath}`); + } catch (err) { + if (err.code === "ENOENT") { + console.error("File non trovato:", iconPath); + } else { + console.error("Errore durante la cancellazione:", err); + } + } +} + + + + + + + + +function parseSite(folder) { + const sitePath = path.join(root, folder); + let shortName = folder; // fallback + let iconPath = null; + + // 1. Cerca manifest.json + const manifestPath = path.join(sitePath, "manifest.json"); + if (fs.existsSync(manifestPath)) { + try { + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + if (manifest.short_name) shortName = manifest.short_name; + if (manifest.icons && manifest.icons.length > 0) { + iconPath = path.join(sitePath, manifest.icons[0].src); + } + } catch (e) {} + } + + // 2. Se non c’è manifest, prova index.html + const indexPath = path.join(sitePath, "index.html"); + if (!iconPath && fs.existsSync(indexPath)) { + const html = fs.readFileSync(indexPath, "utf8"); + const titleMatch = html.match(/(.*?)<\/title>/i); + if (titleMatch) shortName = titleMatch[1]; + + const iconMatch = html.match(/<link[^>]+rel=["']icon["'][^>]+href=["']([^"']+)["']/i); + if (iconMatch) { + iconPath = path.join(sitePath, iconMatch[1]); + } + } + + // 3. Fallback su favicon.ico + const faviconPath = path.join(sitePath, "favicon.ico"); + if (!iconPath && fs.existsSync(faviconPath)) { + iconPath = faviconPath; + } + + // 4. Copia l’icona nella cartella comune + let finalIcon = "icons/default.png"; // path pulito + if (iconPath && fs.existsSync(iconPath)) { + const ext = path.extname(iconPath) || ".png"; + const dest = path.join(iconsDir, `${folder}${ext}`); + try { + fs.copyFileSync(iconPath, dest); + finalIcon = `icons/${folder}${ext}`; // solo riferimento relativo + } catch (e) { + console.error(`Errore copia icona per ${folder}:`, e); + } + } + + return { dir: folder, name: shortName, icon: finalIcon }; +} + +export function generateSitesJson() { + // crea la cartella icons se non esiste + if (!fs.existsSync(iconsDir)) { + fs.mkdirSync(iconsDir, { recursive: true }); + } + + // Scansiona tutte le sottocartelle (escludendo home e icons) + const sites = fs.readdirSync(root).filter(f => + fs.statSync(path.join(root, f)).isDirectory() && f !== "home" && f !== "icons" + ); + console.log("leggo i siti:", sites); + //console.log("primo sito", parseSite(sites[0])); + // Estrarre info + const results = sites.map(parseSite); + + // Salva in JSON + fs.writeFileSync(outputFile, JSON.stringify(results, null, 2), "utf8"); + results.unshift({ dir: 'home', name: 'Home', icon: 'home.ico' }); + console.log(`✅ File JSON salvato in ${outputFile}`); + return results; +} + +// elimina dal file sites.json gli oggetti con dir = "a" +export function delFieldDir(dir) { + if (!fs.existsSync(outputFile)) { + console.error("❌ File non trovato:", outputFile); + return; + } + + try { + const data = fs.readFileSync(outputFile, "utf8"); + const results = JSON.parse(data); + + const filt = results.filter(item => item.dir == dir); + //console.log("icona da canc: ",filt); + console.log("icona da canc: ",filt[0].icon); + deleteIconSync(filt[0].icon); + // filtra via gli elementi con directory = dir + const filtered = results.filter(item => item.dir !== dir); + //console.log("nuovo sites.json", filtered); + fs.writeFileSync(outputFile, JSON.stringify(filtered, null, 2), "utf8"); + console.log(`✅ File aggiornato: rimossi i campi con dir = ${dir}`); + filtered.unshift({ dir: 'home', name: 'Home', icon: 'home.ico' }); + return filtered; + } catch (e) { + console.error("Errore durante la modifica del file:", e); + } +} + +export function addFieldDir(dir) { + if (!fs.existsSync(outputFile)) { + console.error("❌ File non trovato:", outputFile); + return; + } + + try { + const data = fs.readFileSync(outputFile, "utf8"); + const results = JSON.parse(data); + //console.log("sito agg",parseSite(dir)); + // aggiungi elemonto elementi con directory = dir + results.push(parseSite(dir)); + //console.log("add site nuovo sites.json", results); + //const filtered = results.filter(item => item.dir !== dir); + //console.log("nuovo sites.json", filtered); + fs.writeFileSync(outputFile, JSON.stringify(results, null, 2), "utf8"); + results.unshift({ dir: 'home', name: 'Home', icon: 'home.ico' }); + console.log(`✅ File aggiornato: rimossi i campi con dir = ${dir}`); + return results; + } catch (e) { + console.error("Errore durante la modifica del file:", e); + } +} diff --git a/home/icons/composerize.ico b/home/icons/composerize.ico new file mode 100644 index 0000000..9f5a56d Binary files /dev/null and b/home/icons/composerize.ico differ diff --git a/home/icons/composerize.png b/home/icons/composerize.png new file mode 100644 index 0000000..dd3a84a Binary files /dev/null and b/home/icons/composerize.png differ diff --git a/home/icons/composeverter.ico b/home/icons/composeverter.ico new file mode 100644 index 0000000..9f5a56d Binary files /dev/null and b/home/icons/composeverter.ico differ diff --git a/home/icons/composeverter.png b/home/icons/composeverter.png new file mode 100644 index 0000000..dd3a84a Binary files /dev/null and b/home/icons/composeverter.png differ diff --git a/home/icons/composterize.ico b/home/icons/composterize.ico new file mode 100644 index 0000000..9f5a56d Binary files /dev/null and b/home/icons/composterize.ico differ diff --git a/home/icons/decomposerize.ico b/home/icons/decomposerize.ico new file mode 100644 index 0000000..9f5a56d Binary files /dev/null and b/home/icons/decomposerize.ico differ diff --git a/home/icons/decomposerize.png b/home/icons/decomposerize.png new file mode 100644 index 0000000..dd3a84a Binary files /dev/null and b/home/icons/decomposerize.png differ diff --git a/home/index.html b/home/index.html new file mode 100644 index 0000000..8b9ab3b --- /dev/null +++ b/home/index.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<html lang="it"> +<head> + <meta charset="UTF-8"> + <title>Sites Dashboard + + + +

Sites Dashboard

+
+ + + + diff --git a/home/manifest-editor.html b/home/manifest-editor.html new file mode 100644 index 0000000..fcda0dc --- /dev/null +++ b/home/manifest-editor.html @@ -0,0 +1,319 @@ + + + + + + Editor Manifest PWA + + + + + + + + +

Editor Manifest PWA

+ +
Caricamento manifest...
+ +
+
+ Campi principali + + + + + + +
+ + +
+ + + +
+ +
+ Icone + +
+ +

Aggiungi nuova icona

+
+ + + +
+ + Suggerito: carica un’immagine grande (es. 1024×1024) — il server genererà automaticamente 192×192 e 512×512. + +
+ +
+ + +
+
+ + + + + + diff --git a/home/mod.png b/home/mod.png new file mode 100644 index 0000000..fd4c5cb Binary files /dev/null and b/home/mod.png differ diff --git a/home/myicons/browserconfig.xml b/home/myicons/browserconfig.xml new file mode 100644 index 0000000..0a342de --- /dev/null +++ b/home/myicons/browserconfig.xml @@ -0,0 +1,12 @@ + + + + + + + + + #ffffff + + + diff --git a/home/myicons/favicon-114x114.png b/home/myicons/favicon-114x114.png new file mode 100644 index 0000000..c29ae03 Binary files /dev/null and b/home/myicons/favicon-114x114.png differ diff --git a/home/myicons/favicon-120x120.png b/home/myicons/favicon-120x120.png new file mode 100644 index 0000000..49779df Binary files /dev/null and b/home/myicons/favicon-120x120.png differ diff --git a/home/myicons/favicon-128x128.png b/home/myicons/favicon-128x128.png new file mode 100644 index 0000000..375fa62 Binary files /dev/null and b/home/myicons/favicon-128x128.png differ diff --git a/home/myicons/favicon-144x144.png b/home/myicons/favicon-144x144.png new file mode 100644 index 0000000..591c8a7 Binary files /dev/null and b/home/myicons/favicon-144x144.png differ diff --git a/home/myicons/favicon-150x150.png b/home/myicons/favicon-150x150.png new file mode 100644 index 0000000..122a239 Binary files /dev/null and b/home/myicons/favicon-150x150.png differ diff --git a/home/myicons/favicon-152x152.png b/home/myicons/favicon-152x152.png new file mode 100644 index 0000000..b5c49ee Binary files /dev/null and b/home/myicons/favicon-152x152.png differ diff --git a/home/myicons/favicon-16x16.png b/home/myicons/favicon-16x16.png new file mode 100644 index 0000000..a0e3f3d Binary files /dev/null and b/home/myicons/favicon-16x16.png differ diff --git a/home/myicons/favicon-180x180.png b/home/myicons/favicon-180x180.png new file mode 100644 index 0000000..dbf99d2 Binary files /dev/null and b/home/myicons/favicon-180x180.png differ diff --git a/home/myicons/favicon-192x192.png b/home/myicons/favicon-192x192.png new file mode 100644 index 0000000..b78b613 Binary files /dev/null and b/home/myicons/favicon-192x192.png differ diff --git a/home/myicons/favicon-310x310.png b/home/myicons/favicon-310x310.png new file mode 100644 index 0000000..a3ce2cc Binary files /dev/null and b/home/myicons/favicon-310x310.png differ diff --git a/home/myicons/favicon-32x32.png b/home/myicons/favicon-32x32.png new file mode 100644 index 0000000..3826a64 Binary files /dev/null and b/home/myicons/favicon-32x32.png differ diff --git a/home/myicons/favicon-384x384.png b/home/myicons/favicon-384x384.png new file mode 100644 index 0000000..f6db678 Binary files /dev/null and b/home/myicons/favicon-384x384.png differ diff --git a/home/myicons/favicon-512x512.png b/home/myicons/favicon-512x512.png new file mode 100644 index 0000000..6bdec47 Binary files /dev/null and b/home/myicons/favicon-512x512.png differ diff --git a/home/myicons/favicon-57x57.png b/home/myicons/favicon-57x57.png new file mode 100644 index 0000000..2564ad5 Binary files /dev/null and b/home/myicons/favicon-57x57.png differ diff --git a/home/myicons/favicon-60x60.png b/home/myicons/favicon-60x60.png new file mode 100644 index 0000000..98bd2a1 Binary files /dev/null and b/home/myicons/favicon-60x60.png differ diff --git a/home/myicons/favicon-70x70.png b/home/myicons/favicon-70x70.png new file mode 100644 index 0000000..2a1aa8e Binary files /dev/null and b/home/myicons/favicon-70x70.png differ diff --git a/home/myicons/favicon-72x72.png b/home/myicons/favicon-72x72.png new file mode 100644 index 0000000..d36e4c9 Binary files /dev/null and b/home/myicons/favicon-72x72.png differ diff --git a/home/myicons/favicon-76x76.png b/home/myicons/favicon-76x76.png new file mode 100644 index 0000000..6ba6db4 Binary files /dev/null and b/home/myicons/favicon-76x76.png differ diff --git a/home/myicons/favicon-96x96.png b/home/myicons/favicon-96x96.png new file mode 100644 index 0000000..90418a2 Binary files /dev/null and b/home/myicons/favicon-96x96.png differ diff --git a/home/myicons/favicon.ico b/home/myicons/favicon.ico new file mode 100644 index 0000000..59bc512 Binary files /dev/null and b/home/myicons/favicon.ico differ diff --git a/home/myicons/manifest.json b/home/myicons/manifest.json new file mode 100644 index 0000000..50714b1 --- /dev/null +++ b/home/myicons/manifest.json @@ -0,0 +1,60 @@ +{ + "name": "My Sites", + "description": "I miei Siti", + "short_name": "MySites", + "icons": [ + { + "src": "/myicons/favicon-72x72.png", + "type": "image/png", + "sizes": "72x72", + "purpose": "any maskable" + }, + { + "src": "/myicons/favicon-96x96.png", + "type": "image/png", + "sizes": "96x96", + "purpose": "any maskable" + }, + { + "src": "/myicons/favicon-128x128.png", + "type": "image/png", + "sizes": "128x128", + "purpose": "any maskable" + }, + { + "src": "/myicons/favicon-144x144.png", + "type": "image/png", + "sizes": "144x144", + "purpose": "any maskable" + }, + { + "src": "/myicons/favicon-152x152.png", + "type": "image/png", + "sizes": "152x152", + "purpose": "any maskable" + }, + { + "src": "/myicons/favicon-192x192.png", + "type": "image/png", + "sizes": "192x192", + "purpose": "any maskable" + }, + { + "src": "/myicons/favicon-384x384.png", + "type": "image/png", + "sizes": "384x384", + "purpose": "any maskable" + }, + { + "src": "/myicons/favicon-512x512.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "any maskable" + } + ], + "scope": "/", + "start_url": "/?source=pwa", + "display": "standalone", + "theme_color": "#ffffff", + "background_color": "#ffffff" +} \ No newline at end of file diff --git a/home/settings.html b/home/settings.html new file mode 100644 index 0000000..cacd853 --- /dev/null +++ b/home/settings.html @@ -0,0 +1,242 @@ + + + + + + Sites Dashboard + + + +

Sites Settings

+
+ + + + + + + + + diff --git a/home/sidebar.html b/home/sidebar.html new file mode 100644 index 0000000..e27e76d --- /dev/null +++ b/home/sidebar.html @@ -0,0 +1,456 @@ + + + + + + Siti vari + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ +
+
+ Il sito non può essere caricato in iframe.
+ Potrebbe avere X-Frame-Options o Content-Security-Policy che ne impediscono l’incorporamento. +
+
+
+
+ + + + diff --git a/home/sites.json b/home/sites.json new file mode 100644 index 0000000..946aa6a --- /dev/null +++ b/home/sites.json @@ -0,0 +1,17 @@ +[ + { + "dir": "composerize", + "name": "Composerize", + "icon": "icons/composerize.png" + }, + { + "dir": "composeverter", + "name": "Composeverter", + "icon": "icons/composeverter.png" + }, + { + "dir": "decomposerize", + "name": "Decomposerize", + "icon": "icons/decomposerize.png" + } +] \ No newline at end of file diff --git a/home/style.css b/home/style.css new file mode 100644 index 0000000..b1f6bbe --- /dev/null +++ b/home/style.css @@ -0,0 +1,63 @@ +body { + font-family: sans-serif; + margin: 20px; + background: #f5f5f5; +} + +h1 { + text-align: center; + margin-bottom: 30px; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 20px; +} + +.card { + background: white; + border-radius: 8px; + padding: 15px; + text-align: center; + box-shadow: 0 2px 6px rgba(0,0,0,0.1); +} + +.card img { + width: 64px; + height: 64px; + object-fit: contain; + margin-bottom: 10px; +} + +.title { + font-size: 14px; + font-weight: bold; + color: #333; +} +.card.add-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border: 2px dashed #aaa; + cursor: pointer; +} + +.plus-icon { + font-size: 2em; + font-weight: bold; + color: #333; +} + +.modal.hidden { display: none; } +.modal { + position: fixed; top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.5); + display: flex; align-items: center; justify-content: center; +} +.modal-content { + background: #fff; padding: 20px; border-radius: 8px; + width: 300px; +} +.actions { margin-top: 10px; display: flex; gap: 10px; } diff --git a/home/style1.css b/home/style1.css new file mode 100644 index 0000000..d587fd1 --- /dev/null +++ b/home/style1.css @@ -0,0 +1,134 @@ + +body { + font-family: sans-serif; + margin: 20px; + background: #f5f5f5; +} + +h1 { + text-align: center; + margin-bottom: 30px; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 20px; +} + +.card { + background: white; + border-radius: 8px; + padding: 15px; + text-align: center; + box-shadow: 0 2px 6px rgba(0,0,0,0.1); + transition: transform 0.15s ease, box-shadow 0.15s ease; + cursor: pointer; +} + +.card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 10px rgba(0,0,0,0.15); +} + +.card img { + width: 64px; + height: 64px; + object-fit: contain; + margin-bottom: 10px; +} + +.title { + font-size: 14px; + font-weight: bold; + color: #333; +} + +.card.add-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border: 2px dashed #aaa; + cursor: pointer; +} + +.card.add-card:hover { + background: #fafafa; + border-color: #888; +} + +.plus-icon { + font-size: 2em; + font-weight: bold; + color: #333; +} + +/* Modali */ +.modal.hidden { display: none; } +.modal { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal-content { + background: #fff; + padding: 20px; + border-radius: 8px; + width: 300px; + box-shadow: 0 10px 30px rgba(0,0,0,0.2); +} + +.modal-content h2 { + margin-top: 0; + font-size: 18px; + margin-bottom: 15px; +} + +.modal-content label { + display: block; + margin-bottom: 10px; + font-size: 14px; +} + +.modal-content input { + width: 100%; + padding: 6px 8px; + margin-top: 4px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; +} + +.actions { + margin-top: 15px; + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.actions button { + padding: 8px 14px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +.actions button:hover { + opacity: 0.9; +} + +.actions .danger { + background: #e03a3a; + color: #fff; +} + +.actions button:not(.danger) { + background: #eee; + color: #333; diff --git a/make_server_docker.sh b/make_server_docker.sh new file mode 100755 index 0000000..8d9f80f --- /dev/null +++ b/make_server_docker.sh @@ -0,0 +1,5 @@ +#!/bin/bash +echo "commento queste righe" +grep -n "dotenv" server.js | grep -v "//" +sed '/dotenv/ s/^/\/\//' server.js > server_docker.js +echo "Creato server_docker.js con le righe commentate." diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7b614f4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1628 @@ +{ + "name": "dash4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "body-parser": "^2.2.1", + "child_process": "^1.0.2", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "fs-extra": "^11.3.2", + "multer": "^2.0.2", + "sharp": "^0.34.5" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/child_process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", + "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==", + "license": "ISC" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "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", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9b75d74 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "type": "module", + "dependencies": { + "body-parser": "^2.2.1", + "child_process": "^1.0.2", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "fs-extra": "^11.3.2", + "multer": "^2.0.2", + "sharp": "^0.34.5" + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..72da2f0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +requests~=2.32.4 +beautifulsoup4~=4.13.4 +wget~=3.2 +urllib3~=2.5.0 \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..f4a1a25 --- /dev/null +++ b/server.js @@ -0,0 +1,330 @@ +import express from "express"; +import dotenv from "dotenv"; +import path from "path"; +import { fileURLToPath } from "url"; +import fs from "fs"; +import { generateSitesJson, delFieldDir, addFieldDir } from "./generateSitesJson.js"; + +import bodyParser from "body-parser"; +import { execFile } from "child_process"; + +import fse from "fs-extra"; + + +import multer from 'multer'; +import sharp from 'sharp'; + + +let sites = generateSitesJson(); +//sites.unshift({ dir: 'home', name: 'Home', icon: 'home.ico' }); +//console.log(sites); + +//dotenv.config(); +dotenv.config({ path: './.env' }); +const app = express(); +app.use(bodyParser.json()); + +// ricostruisci __dirname in ambiente ESM +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Root sicura per le cartelle dei siti: ./sites +const SITES_ROOT = path.resolve(__dirname,'home' , 'sites'); +const HOME_DIR = path.resolve(__dirname, 'home'); +const SITES_JSON = path.resolve(HOME_DIR, 'sites.json'); +const DOWNLOAD_SITES = 'home/sites'; + +// Leggi variabili da .env +const HOST = process.env.HOST || "0.0.0.0"; +const PORT = process.env.PORT || 3000; +const TYPE = process.env.TYPE || "http"; +const URL = process.env.URL || "https://sites.patachina2.casacam.net"; + +/** crea la dir icons se non esiste **/ +const iconsDir = path.join(__dirname, 'home', 'icons'); +fs.mkdirSync(iconsDir, { recursive: true }); + + +/** Util: carica il manifest con fallback **/ +function readManifest(mPath) { + try { + const text = fs.readFileSync(mPath, 'utf8'); + return JSON.parse(text); + } catch (e) { + return {}; + } +} + +/** Risolve un path del sito sotto SITES_ROOT, impedendo traversal. */ +function resolveSitePathSafe(dir) { + //const clean = normalizeDirField(dir); + //const abs = path.resolve(SITES_ROOT, clean); + const abs = path.resolve(SITES_ROOT, dir); + const rootWithSep = SITES_ROOT.endsWith(path.sep) ? SITES_ROOT : SITES_ROOT + path.sep; + if (!(abs === SITES_ROOT || abs.startsWith(rootWithSep))) { + throw new Error('Percorso non consentito.'); + } + return abs; +} + +// -------------------- Utilities -------------------- +async function readJsonSafe(file) { + try { + return await fse.readJson(file); + } catch (err) { + if (err.code === 'ENOENT') return []; + throw err; + } +} + +async function writeJsonAtomic(file, data) { + const tmp = file + '.tmp'; + await fse.writeJson(tmp, data, { spaces: 2 }); + await fse.move(tmp, file, { overwrite: true }); +} + +app.post('/del-site', async (req, res) => { + try { + let { dir } = req.body || {}; + //console.log("dir da cancellare: ",dir); + const siteAbsPath = resolveSitePathSafe(dir); + //console.log("path assoluto: ", siteAbsPath); + if (await fse.pathExists(siteAbsPath)) { + await fse.remove(siteAbsPath); + } + + sites = delFieldDir(dir); + //await deleteIcon("icons/composerize.ico"); + console.log(`la dir=${dir} è stata cancellata`); + return res.json({ + ok: true, +// removed: { name: removed?.name, dir: removed?.dir }, + deletedPath: siteAbsPath + }); + } catch (err) { + console.error('del-site error:', err); + const msg = err?.message || 'Errore interno nella cancellazione.'; + return res.status(500).json({ error: msg }); + } +}); + + +app.use((req, res, next) => { + res.removeHeader("X-Frame-Options"); + res.setHeader("Content-Security-Policy", "frame-ancestors *"); + next(); +}); + +// endpoint POST /add-site +app.post("/add-site", (req, res) => { + const { url, dir } = req.body; + const dir1 = `${DOWNLOAD_SITES}/${dir}`; + + // 👉 qui esegui lo script Python + execFile("downloadsite.sh", [url, dir1], (error, stdout, stderr) => { + if (error) { + console.error("Errore:", error); + return res.status(500).json({ status: "error", message: stderr }); + } + sites = addFieldDir(dir); + res.json({ status: "ok", output: stdout }); + }); +}); + + + +//const SITES = []; +// Monta le cartelle statiche in base alla lista SITES +sites.forEach(site => { + if (site) { + // directory reale: sites/ + const dirPath = path.join(SITES_ROOT, site.dir); + + // URL pubblico: / + app.use(`/${site.dir}`, express.static(dirPath)); + } +}); +// Cartella public come root +//app.use("/", express.static("public")); +app.use("/", express.static("home", { index: "sidebar.html" })); +//app.use("/root", express.static("public")); +app.use("/home", express.static("home")); +app.use("/settings", express.static("home", { index: "settings.html" })); +app.use("/manifest", express.static("home", { index: "manifest-editor.html" })); +/* +app.use('/manifest', express.static(path.join(process.cwd(), 'home'), { + index: 'manifest-editor.html', + redirect: false // opzionale: evita 301 /manifest -> /manifest/ +})); +*/ + +// Endpoint per esporre la config al client +app.get("/config.json", (req, res) => { + res.json({ + host: HOST, + port: PORT, +// sites: SITES, + s: sites, + url: URL + }); +}); + +const upload1 = multer(); + +/** GET: manifest corrente **/ +app.post('/api/manifest', upload1.none(), (req, res) => { +//app.get('/api/manifest', (req, res) => { + try { + //console.log(req.body.dir); + const mydir = req.body.dir; + const manifest = readManifest(path.join(SITES_ROOT, mydir , 'manifest.json')); + res.json(manifest); + } catch (e) { + res.status(500).json({ error: 'Impossibile leggere manifest.json' }); + } +}); + +/** PUT: salva il manifest (campi + icone) **/ +app.put('/api/manifest', upload1.none(), (req, res) => { + // Validazioni essenziali + const mydir = req.body.dir; + const incoming = JSON.parse(req.body.manifest); + //console.log("manifest put"); + //console.log("incoming=",incoming); + //console.log("mydir=",mydir); + const manPath = path.join(SITES_ROOT, mydir , 'manifest.json'); + //console.log("manPath",manPath); + if (!incoming.name || !incoming.short_name) { + return res.status(400).json({ error: 'name e short_name sono obbligatori' }); + } + //console.log("step1"); + if (!Array.isArray(incoming.icons)) incoming.icons = []; + //console.log("step2"); + try { + fs.writeFileSync(manPath, JSON.stringify(incoming, null, 2), 'utf8'); + console.log(`manifest di <${mydir}> salvato`); + generateSitesJson(); + res.json({ ok: true }); + } catch (e) { + console.error(e); + res.status(500).json({ error: 'Salvataggio manifest fallito' }); + } +}); + +/** Multer: configurazione upload (in memoria) **/ +const upload = multer({ + storage: multer.memoryStorage(), + fileFilter: (req, file, cb) => { + // Accetta i tipi più comuni + const okTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/jpg']; + if (file && okTypes.includes(file.mimetype)) { + cb(null, true); + } else { + cb(new Error(`Formato non supportato: ${file ? file.mimetype : 'Nessun file'}`)); + } + }, + limits: { fileSize: 8 * 1024 * 1024 } // 8 MB +}); + +/** POST: upload icona + generazione 192 e 512 **/ +app.post('/api/upload-icon', upload.single('icon'), async (req, res) => { + try { + // Log diagnostico utile +/* console.log('Upload Content-Type:', req.headers['content-type']); + console.log('Upload body keys:', Object.keys(req.body || {})); + console.log('Upload file:', req.file && { + fieldname: req.file.fieldname, + mimetype: req.file.mimetype, + size: req.file.size, + originalname: req.file.originalname + }); +*/ + const mydir = req.body.mydir; + //console.log("dir=",mydir); + + const iconsMyDir = path.join(SITES_ROOT,mydir , 'icons'); + fs.mkdirSync(iconsMyDir, { recursive: true }); + + if (!req.file) { + return res.status(400).json({ + error: 'Nessun file ricevuto nel campo "icon". Assicurati che l’input abbia name="icon" e che il JS usi FormData.append("icon", file).' + }); + } + + const purpose = (req.body.purpose || 'any').trim(); + const baseNameSafe = (req.file.originalname || 'icon') + .replace(/\.[^.]+$/, '') // togli estensione + .replace(/[^a-zA-Z0-9-_]/g, ''); // pulisci caratteri strani + + // Puoi aggiungere altre dimensioni se vuoi + const sizes = [192, 512]; + const created = []; + + for (const size of sizes) { + const filename = `${baseNameSafe}-${size}.png`; + const outPath = path.join(path.join(SITES_ROOT, mydir, 'icons'), filename); + + await sharp(req.file.buffer) + .resize(size, size, { fit: 'cover' }) + .png({ quality: 90 }) + .toFile(outPath); + + created.push({ + src: `/icons/${filename}`, + sizes: `${size}x${size}`, + type: 'image/png', + purpose + }); + } + generateSitesJson(); + res.json({ ok: true, icons: created }); + } catch (e) { + console.error('Errore /api/upload-icon:', e); + res.status(500).json({ error: 'Elaborazione icona fallita' }); + } +}); + + +/** + * DELETE: rimuove icona dal manifest e opzionalmente elimina il file + * query: + * - src (richiesto): percorso icona (es. /icons/icon-192.png) + * - removeFile=true|false (opzionale): se true elimina anche il file se sotto /public/icons + */ +app.delete('/api/icons', (req, res) => { + const src = req.query.src; + const removeFile = (req.query.removeFile || 'false') === 'true'; + const mydir = req.query.myDir; + const manPath = path.join(SITES_ROOT, mydir , 'manifest.json'); + //console.log("delete icon:"); + //console.log("src=",src); + //console.log("mydir=",mydir); + if (!src) return res.status(400).json({ error: 'Parametro src mancante' }); + + try { + const manifest = readManifest(manPath); + manifest.icons = (manifest.icons || []).filter(i => i.src !== src); + + if (removeFile) { + const absFilePath = path.join(SITES_ROOT, mydir, src.replace(/^\//, '')); + const isUnderIcons = absFilePath.startsWith(iconsDir + path.sep); + if (isUnderIcons && fs.existsSync(absFilePath)) { + fs.unlinkSync(absFilePath); + } + } + + fs.writeFileSync(manPath, JSON.stringify(manifest, null, 2), 'utf8'); + generateSitesJson(); + res.json({ ok: true, removedFromManifest: true, fileDeleted: removeFile }); + } catch (e) { + console.error(e); + res.status(500).json({ error: 'Eliminazione icona fallita' }); + } +}); + + + +app.listen(PORT, HOST, () => { + console.log(`✅ Server pronto su http://${HOST}:${PORT}`); +}); + diff --git a/server_docker.js b/server_docker.js new file mode 100644 index 0000000..067add3 --- /dev/null +++ b/server_docker.js @@ -0,0 +1,330 @@ +import express from "express"; +//import dotenv from "dotenv"; +import path from "path"; +import { fileURLToPath } from "url"; +import fs from "fs"; +import { generateSitesJson, delFieldDir, addFieldDir } from "./generateSitesJson.js"; + +import bodyParser from "body-parser"; +import { execFile } from "child_process"; + +import fse from "fs-extra"; + + +import multer from 'multer'; +import sharp from 'sharp'; + + +let sites = generateSitesJson(); +//sites.unshift({ dir: 'home', name: 'Home', icon: 'home.ico' }); +//console.log(sites); + +////dotenv.config(); +//dotenv.config({ path: './.env' }); +const app = express(); +app.use(bodyParser.json()); + +// ricostruisci __dirname in ambiente ESM +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Root sicura per le cartelle dei siti: ./sites +const SITES_ROOT = path.resolve(__dirname,'home' , 'sites'); +const HOME_DIR = path.resolve(__dirname, 'home'); +const SITES_JSON = path.resolve(HOME_DIR, 'sites.json'); +const DOWNLOAD_SITES = 'home/sites'; + +// Leggi variabili da .env +const HOST = process.env.HOST || "0.0.0.0"; +const PORT = process.env.PORT || 3000; +const TYPE = process.env.TYPE || "http"; +const URL = process.env.URL || "https://sites.patachina2.casacam.net"; + +/** crea la dir icons se non esiste **/ +const iconsDir = path.join(__dirname, 'home', 'icons'); +fs.mkdirSync(iconsDir, { recursive: true }); + + +/** Util: carica il manifest con fallback **/ +function readManifest(mPath) { + try { + const text = fs.readFileSync(mPath, 'utf8'); + return JSON.parse(text); + } catch (e) { + return {}; + } +} + +/** Risolve un path del sito sotto SITES_ROOT, impedendo traversal. */ +function resolveSitePathSafe(dir) { + //const clean = normalizeDirField(dir); + //const abs = path.resolve(SITES_ROOT, clean); + const abs = path.resolve(SITES_ROOT, dir); + const rootWithSep = SITES_ROOT.endsWith(path.sep) ? SITES_ROOT : SITES_ROOT + path.sep; + if (!(abs === SITES_ROOT || abs.startsWith(rootWithSep))) { + throw new Error('Percorso non consentito.'); + } + return abs; +} + +// -------------------- Utilities -------------------- +async function readJsonSafe(file) { + try { + return await fse.readJson(file); + } catch (err) { + if (err.code === 'ENOENT') return []; + throw err; + } +} + +async function writeJsonAtomic(file, data) { + const tmp = file + '.tmp'; + await fse.writeJson(tmp, data, { spaces: 2 }); + await fse.move(tmp, file, { overwrite: true }); +} + +app.post('/del-site', async (req, res) => { + try { + let { dir } = req.body || {}; + //console.log("dir da cancellare: ",dir); + const siteAbsPath = resolveSitePathSafe(dir); + //console.log("path assoluto: ", siteAbsPath); + if (await fse.pathExists(siteAbsPath)) { + await fse.remove(siteAbsPath); + } + + sites = delFieldDir(dir); + //await deleteIcon("icons/composerize.ico"); + console.log(`la dir=${dir} è stata cancellata`); + return res.json({ + ok: true, +// removed: { name: removed?.name, dir: removed?.dir }, + deletedPath: siteAbsPath + }); + } catch (err) { + console.error('del-site error:', err); + const msg = err?.message || 'Errore interno nella cancellazione.'; + return res.status(500).json({ error: msg }); + } +}); + + +app.use((req, res, next) => { + res.removeHeader("X-Frame-Options"); + res.setHeader("Content-Security-Policy", "frame-ancestors *"); + next(); +}); + +// endpoint POST /add-site +app.post("/add-site", (req, res) => { + const { url, dir } = req.body; + const dir1 = `${DOWNLOAD_SITES}/${dir}`; + + // 👉 qui esegui lo script Python + execFile("downloadsite.sh", [url, dir1], (error, stdout, stderr) => { + if (error) { + console.error("Errore:", error); + return res.status(500).json({ status: "error", message: stderr }); + } + sites = addFieldDir(dir); + res.json({ status: "ok", output: stdout }); + }); +}); + + + +//const SITES = []; +// Monta le cartelle statiche in base alla lista SITES +sites.forEach(site => { + if (site) { + // directory reale: sites/ + const dirPath = path.join(SITES_ROOT, site.dir); + + // URL pubblico: / + app.use(`/${site.dir}`, express.static(dirPath)); + } +}); +// Cartella public come root +//app.use("/", express.static("public")); +app.use("/", express.static("home", { index: "sidebar.html" })); +//app.use("/root", express.static("public")); +app.use("/home", express.static("home")); +app.use("/settings", express.static("home", { index: "settings.html" })); +app.use("/manifest", express.static("home", { index: "manifest-editor.html" })); +/* +app.use('/manifest', express.static(path.join(process.cwd(), 'home'), { + index: 'manifest-editor.html', + redirect: false // opzionale: evita 301 /manifest -> /manifest/ +})); +*/ + +// Endpoint per esporre la config al client +app.get("/config.json", (req, res) => { + res.json({ + host: HOST, + port: PORT, +// sites: SITES, + s: sites, + url: URL + }); +}); + +const upload1 = multer(); + +/** GET: manifest corrente **/ +app.post('/api/manifest', upload1.none(), (req, res) => { +//app.get('/api/manifest', (req, res) => { + try { + //console.log(req.body.dir); + const mydir = req.body.dir; + const manifest = readManifest(path.join(SITES_ROOT, mydir , 'manifest.json')); + res.json(manifest); + } catch (e) { + res.status(500).json({ error: 'Impossibile leggere manifest.json' }); + } +}); + +/** PUT: salva il manifest (campi + icone) **/ +app.put('/api/manifest', upload1.none(), (req, res) => { + // Validazioni essenziali + const mydir = req.body.dir; + const incoming = JSON.parse(req.body.manifest); + //console.log("manifest put"); + //console.log("incoming=",incoming); + //console.log("mydir=",mydir); + const manPath = path.join(SITES_ROOT, mydir , 'manifest.json'); + //console.log("manPath",manPath); + if (!incoming.name || !incoming.short_name) { + return res.status(400).json({ error: 'name e short_name sono obbligatori' }); + } + //console.log("step1"); + if (!Array.isArray(incoming.icons)) incoming.icons = []; + //console.log("step2"); + try { + fs.writeFileSync(manPath, JSON.stringify(incoming, null, 2), 'utf8'); + console.log(`manifest di <${mydir}> salvato`); + generateSitesJson(); + res.json({ ok: true }); + } catch (e) { + console.error(e); + res.status(500).json({ error: 'Salvataggio manifest fallito' }); + } +}); + +/** Multer: configurazione upload (in memoria) **/ +const upload = multer({ + storage: multer.memoryStorage(), + fileFilter: (req, file, cb) => { + // Accetta i tipi più comuni + const okTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/jpg']; + if (file && okTypes.includes(file.mimetype)) { + cb(null, true); + } else { + cb(new Error(`Formato non supportato: ${file ? file.mimetype : 'Nessun file'}`)); + } + }, + limits: { fileSize: 8 * 1024 * 1024 } // 8 MB +}); + +/** POST: upload icona + generazione 192 e 512 **/ +app.post('/api/upload-icon', upload.single('icon'), async (req, res) => { + try { + // Log diagnostico utile +/* console.log('Upload Content-Type:', req.headers['content-type']); + console.log('Upload body keys:', Object.keys(req.body || {})); + console.log('Upload file:', req.file && { + fieldname: req.file.fieldname, + mimetype: req.file.mimetype, + size: req.file.size, + originalname: req.file.originalname + }); +*/ + const mydir = req.body.mydir; + //console.log("dir=",mydir); + + const iconsMyDir = path.join(SITES_ROOT,mydir , 'icons'); + fs.mkdirSync(iconsMyDir, { recursive: true }); + + if (!req.file) { + return res.status(400).json({ + error: 'Nessun file ricevuto nel campo "icon". Assicurati che l’input abbia name="icon" e che il JS usi FormData.append("icon", file).' + }); + } + + const purpose = (req.body.purpose || 'any').trim(); + const baseNameSafe = (req.file.originalname || 'icon') + .replace(/\.[^.]+$/, '') // togli estensione + .replace(/[^a-zA-Z0-9-_]/g, ''); // pulisci caratteri strani + + // Puoi aggiungere altre dimensioni se vuoi + const sizes = [192, 512]; + const created = []; + + for (const size of sizes) { + const filename = `${baseNameSafe}-${size}.png`; + const outPath = path.join(path.join(SITES_ROOT, mydir, 'icons'), filename); + + await sharp(req.file.buffer) + .resize(size, size, { fit: 'cover' }) + .png({ quality: 90 }) + .toFile(outPath); + + created.push({ + src: `/icons/${filename}`, + sizes: `${size}x${size}`, + type: 'image/png', + purpose + }); + } + generateSitesJson(); + res.json({ ok: true, icons: created }); + } catch (e) { + console.error('Errore /api/upload-icon:', e); + res.status(500).json({ error: 'Elaborazione icona fallita' }); + } +}); + + +/** + * DELETE: rimuove icona dal manifest e opzionalmente elimina il file + * query: + * - src (richiesto): percorso icona (es. /icons/icon-192.png) + * - removeFile=true|false (opzionale): se true elimina anche il file se sotto /public/icons + */ +app.delete('/api/icons', (req, res) => { + const src = req.query.src; + const removeFile = (req.query.removeFile || 'false') === 'true'; + const mydir = req.query.myDir; + const manPath = path.join(SITES_ROOT, mydir , 'manifest.json'); + //console.log("delete icon:"); + //console.log("src=",src); + //console.log("mydir=",mydir); + if (!src) return res.status(400).json({ error: 'Parametro src mancante' }); + + try { + const manifest = readManifest(manPath); + manifest.icons = (manifest.icons || []).filter(i => i.src !== src); + + if (removeFile) { + const absFilePath = path.join(SITES_ROOT, mydir, src.replace(/^\//, '')); + const isUnderIcons = absFilePath.startsWith(iconsDir + path.sep); + if (isUnderIcons && fs.existsSync(absFilePath)) { + fs.unlinkSync(absFilePath); + } + } + + fs.writeFileSync(manPath, JSON.stringify(manifest, null, 2), 'utf8'); + generateSitesJson(); + res.json({ ok: true, removedFromManifest: true, fileDeleted: removeFile }); + } catch (e) { + console.error(e); + res.status(500).json({ error: 'Eliminazione icona fallita' }); + } +}); + + + +app.listen(PORT, HOST, () => { + console.log(`✅ Server pronto su http://${HOST}:${PORT}`); +}); + diff --git a/web_scraper.log b/web_scraper.log new file mode 100644 index 0000000..4a2d226 --- /dev/null +++ b/web_scraper.log @@ -0,0 +1,219 @@ +07:57:22 | INFO | MainThread | [1/100] https://www.composerize.com +07:57:22 | DEBUG | MainThread | Starting new HTTPS connection (1): www.composerize.com:443 +07:57:22 | DEBUG | MainThread | https://www.composerize.com:443 "GET / HTTP/1.1" 200 867 +07:57:22 | DEBUG | MainThread | Created directory sites/mio +07:57:22 | DEBUG | MainThread | Saved page sites/mio/index.html +07:57:22 | DEBUG | DL-2 | Starting new HTTPS connection (2): www.composerize.com:443 +07:57:22 | DEBUG | DL-1 | Starting new HTTPS connection (3): www.composerize.com:443 +07:57:22 | DEBUG | DL-4 | Starting new HTTPS connection (4): www.composerize.com:443 +07:57:22 | DEBUG | DL-3 | https://www.composerize.com:443 "GET /favicon.ico HTTP/1.1" 200 None +07:57:22 | DEBUG | DL-3 | Saved resource -> sites/mio/favicon.ico +07:57:22 | DEBUG | DL-4 | https://www.composerize.com:443 "GET /manifest.json HTTP/1.1" 200 288 +07:57:22 | DEBUG | DL-4 | Saved resource -> sites/mio/manifest.json +07:57:22 | DEBUG | DL-1 | https://www.composerize.com:443 "GET /static/css/main.757d3484.css HTTP/1.1" 200 None +07:57:22 | DEBUG | DL-1 | Created directory sites/mio/static/css +07:57:22 | DEBUG | DL-1 | Saved resource -> sites/mio/static/css/main.757d3484.css +07:57:22 | DEBUG | DL-2 | https://www.composerize.com:443 "GET /static/js/main.623047e0.js HTTP/1.1" 200 None +07:57:22 | DEBUG | DL-2 | Created directory sites/mio/static/js +07:57:23 | DEBUG | DL-2 | Saved resource -> sites/mio/static/js/main.623047e0.js +07:57:23 | INFO | MainThread | Crawl finished: 1 pages in 1.03s (1.03s avg) +08:00:47 | INFO | MainThread | [1/100] https://www.composerize.com +08:00:47 | DEBUG | MainThread | Starting new HTTPS connection (1): www.composerize.com:443 +08:00:47 | DEBUG | MainThread | https://www.composerize.com:443 "GET / HTTP/1.1" 200 867 +08:00:47 | DEBUG | MainThread | Saved page sites/mio/index.html +08:00:47 | INFO | MainThread | Crawl finished: 1 pages in 0.14s (0.14s avg) +08:11:57 | INFO | MainThread | [1/100] https://www.composerize.com +08:11:57 | DEBUG | MainThread | Starting new HTTPS connection (1): www.composerize.com:443 +08:11:57 | DEBUG | MainThread | https://www.composerize.com:443 "GET / HTTP/1.1" 200 867 +08:11:57 | DEBUG | MainThread | Saved page sites/mio/index.html +08:11:57 | INFO | MainThread | Crawl finished: 1 pages in 0.12s (0.12s avg) +08:23:16 | INFO | MainThread | [1/100] https://www.composerize.com +08:23:16 | DEBUG | MainThread | Starting new HTTPS connection (1): www.composerize.com:443 +08:23:16 | DEBUG | MainThread | https://www.composerize.com:443 "GET / HTTP/1.1" 200 867 +08:23:16 | DEBUG | MainThread | Created directory home/sites/mio +08:23:16 | DEBUG | MainThread | Saved page home/sites/mio/index.html +08:23:16 | DEBUG | DL-3 | Starting new HTTPS connection (2): www.composerize.com:443 +08:23:16 | DEBUG | DL-2 | Starting new HTTPS connection (3): www.composerize.com:443 +08:23:16 | DEBUG | DL-1 | Starting new HTTPS connection (4): www.composerize.com:443 +08:23:16 | DEBUG | DL-4 | https://www.composerize.com:443 "GET /favicon.ico HTTP/1.1" 200 3430 +08:23:16 | DEBUG | DL-4 | Saved resource -> home/sites/mio/favicon.ico +08:23:16 | DEBUG | DL-1 | https://www.composerize.com:443 "GET /manifest.json HTTP/1.1" 200 288 +08:23:16 | DEBUG | DL-1 | Saved resource -> home/sites/mio/manifest.json +08:23:16 | DEBUG | DL-3 | https://www.composerize.com:443 "GET /static/css/main.757d3484.css HTTP/1.1" 200 1761 +08:23:16 | DEBUG | DL-3 | Created directory home/sites/mio/static/css +08:23:16 | DEBUG | DL-3 | Saved resource -> home/sites/mio/static/css/main.757d3484.css +08:23:16 | DEBUG | DL-2 | https://www.composerize.com:443 "GET /static/js/main.623047e0.js HTTP/1.1" 200 176652 +08:23:16 | DEBUG | DL-2 | Created directory home/sites/mio/static/js +08:23:16 | DEBUG | DL-2 | Saved resource -> home/sites/mio/static/js/main.623047e0.js +08:23:16 | INFO | MainThread | Crawl finished: 1 pages in 0.39s (0.39s avg) +08:25:06 | INFO | MainThread | [1/100] https://www.composerize.com +08:25:06 | DEBUG | MainThread | Starting new HTTPS connection (1): www.composerize.com:443 +08:25:06 | DEBUG | MainThread | https://www.composerize.com:443 "GET / HTTP/1.1" 200 867 +08:25:06 | DEBUG | MainThread | Created directory home/sites/mio +08:25:06 | DEBUG | MainThread | Saved page home/sites/mio/index.html +08:25:06 | DEBUG | DL-1 | Starting new HTTPS connection (2): www.composerize.com:443 +08:25:06 | DEBUG | DL-2 | Starting new HTTPS connection (3): www.composerize.com:443 +08:25:06 | DEBUG | DL-4 | Starting new HTTPS connection (4): www.composerize.com:443 +08:25:06 | DEBUG | DL-3 | https://www.composerize.com:443 "GET /manifest.json HTTP/1.1" 200 288 +08:25:06 | DEBUG | DL-3 | Saved resource -> home/sites/mio/manifest.json +08:25:06 | DEBUG | DL-1 | https://www.composerize.com:443 "GET /favicon.ico HTTP/1.1" 200 3430 +08:25:06 | DEBUG | DL-2 | https://www.composerize.com:443 "GET /static/js/main.623047e0.js HTTP/1.1" 200 176652 +08:25:06 | DEBUG | DL-2 | Created directory home/sites/mio/static/js +08:25:06 | DEBUG | DL-1 | Saved resource -> home/sites/mio/favicon.ico +08:25:06 | DEBUG | DL-4 | https://www.composerize.com:443 "GET /static/css/main.757d3484.css HTTP/1.1" 200 1761 +08:25:06 | DEBUG | DL-4 | Created directory home/sites/mio/static/css +08:25:06 | DEBUG | DL-4 | Saved resource -> home/sites/mio/static/css/main.757d3484.css +08:25:07 | DEBUG | DL-2 | Saved resource -> home/sites/mio/static/js/main.623047e0.js +08:25:07 | INFO | MainThread | Crawl finished: 1 pages in 0.30s (0.30s avg) +08:54:46 | INFO | MainThread | [1/100] https://www.composerize.com +08:54:46 | DEBUG | MainThread | Starting new HTTPS connection (1): www.composerize.com:443 +08:54:46 | DEBUG | MainThread | https://www.composerize.com:443 "GET / HTTP/1.1" 200 867 +08:54:46 | DEBUG | MainThread | Created directory home/sites/mio +08:54:46 | DEBUG | DL-3 | Starting new HTTPS connection (2): www.composerize.com:443 +08:54:46 | DEBUG | DL-1 | Starting new HTTPS connection (3): www.composerize.com:443 +08:54:46 | DEBUG | DL-4 | Starting new HTTPS connection (4): www.composerize.com:443 +08:54:46 | DEBUG | MainThread | Saved page home/sites/mio/index.html +08:54:46 | DEBUG | DL-2 | https://www.composerize.com:443 "GET /manifest.json HTTP/1.1" 200 288 +08:54:46 | DEBUG | DL-2 | Saved resource -> home/sites/mio/manifest.json +08:54:46 | DEBUG | DL-4 | https://www.composerize.com:443 "GET /static/js/main.623047e0.js HTTP/1.1" 200 176652 +08:54:46 | DEBUG | DL-1 | https://www.composerize.com:443 "GET /favicon.ico HTTP/1.1" 200 3430 +08:54:46 | DEBUG | DL-4 | Created directory home/sites/mio/static/js +08:54:46 | DEBUG | DL-1 | Saved resource -> home/sites/mio/favicon.ico +08:54:46 | DEBUG | DL-3 | https://www.composerize.com:443 "GET /static/css/main.757d3484.css HTTP/1.1" 200 1761 +08:54:46 | DEBUG | DL-3 | Created directory home/sites/mio/static/css +08:54:46 | DEBUG | DL-3 | Saved resource -> home/sites/mio/static/css/main.757d3484.css +08:54:46 | DEBUG | DL-4 | Saved resource -> home/sites/mio/static/js/main.623047e0.js +08:54:46 | INFO | MainThread | Crawl finished: 1 pages in 0.49s (0.49s avg) +08:57:24 | INFO | MainThread | [1/100] https://www.composerize.com +08:57:24 | DEBUG | MainThread | Starting new HTTPS connection (1): www.composerize.com:443 +08:57:24 | DEBUG | MainThread | https://www.composerize.com:443 "GET / HTTP/1.1" 200 867 +08:57:24 | DEBUG | MainThread | Created directory home/sites/mio +08:57:24 | DEBUG | MainThread | Saved page home/sites/mio/index.html +08:57:24 | DEBUG | DL-3 | Starting new HTTPS connection (2): www.composerize.com:443 +08:57:24 | DEBUG | DL-4 | Starting new HTTPS connection (3): www.composerize.com:443 +08:57:24 | DEBUG | DL-1 | Starting new HTTPS connection (4): www.composerize.com:443 +08:57:24 | DEBUG | DL-2 | https://www.composerize.com:443 "GET /static/js/main.623047e0.js HTTP/1.1" 200 176652 +08:57:24 | DEBUG | DL-2 | Created directory home/sites/mio/static/js +08:57:24 | DEBUG | DL-3 | https://www.composerize.com:443 "GET /favicon.ico HTTP/1.1" 200 3430 +08:57:24 | DEBUG | DL-3 | Saved resource -> home/sites/mio/favicon.ico +08:57:24 | DEBUG | DL-2 | Saved resource -> home/sites/mio/static/js/main.623047e0.js +08:57:24 | DEBUG | DL-4 | https://www.composerize.com:443 "GET /manifest.json HTTP/1.1" 200 288 +08:57:24 | DEBUG | DL-4 | Saved resource -> home/sites/mio/manifest.json +08:57:24 | DEBUG | DL-1 | https://www.composerize.com:443 "GET /static/css/main.757d3484.css HTTP/1.1" 200 1761 +08:57:24 | DEBUG | DL-1 | Created directory home/sites/mio/static/css +08:57:24 | DEBUG | DL-1 | Saved resource -> home/sites/mio/static/css/main.757d3484.css +08:57:24 | INFO | MainThread | Crawl finished: 1 pages in 0.30s (0.30s avg) +09:00:47 | INFO | MainThread | [1/100] https://www.composerize.com +09:00:47 | DEBUG | MainThread | Starting new HTTPS connection (1): www.composerize.com:443 +09:00:47 | DEBUG | MainThread | https://www.composerize.com:443 "GET / HTTP/1.1" 200 867 +09:00:47 | DEBUG | DL-4 | Starting new HTTPS connection (2): www.composerize.com:443 +09:00:47 | DEBUG | DL-2 | Starting new HTTPS connection (3): www.composerize.com:443 +09:00:47 | DEBUG | MainThread | Created directory home/sites/mio +09:00:47 | DEBUG | MainThread | Saved page home/sites/mio/index.html +09:00:47 | DEBUG | DL-1 | Starting new HTTPS connection (4): www.composerize.com:443 +09:00:47 | DEBUG | DL-3 | https://www.composerize.com:443 "GET /favicon.ico HTTP/1.1" 200 3430 +09:00:47 | DEBUG | DL-3 | Saved resource -> home/sites/mio/favicon.ico +09:00:47 | DEBUG | DL-4 | https://www.composerize.com:443 "GET /static/css/main.757d3484.css HTTP/1.1" 200 1761 +09:00:47 | DEBUG | DL-4 | Created directory home/sites/mio/static/css +09:00:47 | DEBUG | DL-4 | Saved resource -> home/sites/mio/static/css/main.757d3484.css +09:00:47 | DEBUG | DL-2 | https://www.composerize.com:443 "GET /manifest.json HTTP/1.1" 200 288 +09:00:47 | DEBUG | DL-2 | Saved resource -> home/sites/mio/manifest.json +09:00:47 | DEBUG | DL-1 | https://www.composerize.com:443 "GET /static/js/main.623047e0.js HTTP/1.1" 200 176652 +09:00:47 | DEBUG | DL-1 | Created directory home/sites/mio/static/js +09:00:47 | DEBUG | DL-1 | Saved resource -> home/sites/mio/static/js/main.623047e0.js +09:00:47 | INFO | MainThread | Crawl finished: 1 pages in 0.40s (0.40s avg) +09:41:16 | INFO | MainThread | [1/100] https://www.composerize.com +09:41:16 | DEBUG | MainThread | Starting new HTTPS connection (1): www.composerize.com:443 +09:41:16 | DEBUG | MainThread | https://www.composerize.com:443 "GET / HTTP/1.1" 200 867 +09:41:16 | DEBUG | MainThread | Created directory home/sites/composterize +09:41:16 | DEBUG | MainThread | Saved page home/sites/composterize/index.html +09:41:16 | DEBUG | DL-4 | Starting new HTTPS connection (2): www.composerize.com:443 +09:41:16 | DEBUG | DL-1 | Starting new HTTPS connection (3): www.composerize.com:443 +09:41:16 | DEBUG | DL-2 | Starting new HTTPS connection (4): www.composerize.com:443 +09:41:16 | DEBUG | DL-3 | https://www.composerize.com:443 "GET /favicon.ico HTTP/1.1" 200 3430 +09:41:16 | DEBUG | DL-3 | Saved resource -> home/sites/composterize/favicon.ico +09:41:16 | DEBUG | DL-4 | https://www.composerize.com:443 "GET /manifest.json HTTP/1.1" 200 288 +09:41:16 | DEBUG | DL-4 | Saved resource -> home/sites/composterize/manifest.json +09:41:16 | DEBUG | DL-2 | https://www.composerize.com:443 "GET /static/js/main.623047e0.js HTTP/1.1" 200 176652 +09:41:16 | DEBUG | DL-2 | Created directory home/sites/composterize/static/js +09:41:16 | DEBUG | DL-1 | https://www.composerize.com:443 "GET /static/css/main.757d3484.css HTTP/1.1" 200 1761 +09:41:16 | DEBUG | DL-1 | Created directory home/sites/composterize/static/css +09:41:16 | DEBUG | DL-1 | Saved resource -> home/sites/composterize/static/css/main.757d3484.css +09:41:16 | DEBUG | DL-2 | Saved resource -> home/sites/composterize/static/js/main.623047e0.js +09:41:16 | INFO | MainThread | Crawl finished: 1 pages in 0.44s (0.44s avg) +09:46:06 | INFO | MainThread | [1/100] https://www.composerize.com +09:46:06 | DEBUG | MainThread | Starting new HTTPS connection (1): www.composerize.com:443 +09:46:06 | DEBUG | MainThread | https://www.composerize.com:443 "GET / HTTP/1.1" 200 867 +09:46:06 | DEBUG | MainThread | Created directory home/sites/composerize +09:46:06 | DEBUG | MainThread | Saved page home/sites/composerize/index.html +09:46:06 | DEBUG | DL-2 | Starting new HTTPS connection (2): www.composerize.com:443 +09:46:06 | DEBUG | DL-1 | Starting new HTTPS connection (3): www.composerize.com:443 +09:46:06 | DEBUG | DL-4 | Starting new HTTPS connection (4): www.composerize.com:443 +09:46:06 | DEBUG | DL-3 | https://www.composerize.com:443 "GET /manifest.json HTTP/1.1" 200 288 +09:46:06 | DEBUG | DL-3 | Saved resource -> home/sites/composerize/manifest.json +09:46:06 | DEBUG | DL-4 | https://www.composerize.com:443 "GET /static/js/main.623047e0.js HTTP/1.1" 200 176652 +09:46:06 | DEBUG | DL-4 | Created directory home/sites/composerize/static/js +09:46:06 | DEBUG | DL-2 | https://www.composerize.com:443 "GET /favicon.ico HTTP/1.1" 200 3430 +09:46:06 | DEBUG | DL-2 | Saved resource -> home/sites/composerize/favicon.ico +09:46:06 | DEBUG | DL-1 | https://www.composerize.com:443 "GET /static/css/main.757d3484.css HTTP/1.1" 200 1761 +09:46:06 | DEBUG | DL-1 | Created directory home/sites/composerize/static/css +09:46:06 | DEBUG | DL-1 | Saved resource -> home/sites/composerize/static/css/main.757d3484.css +09:46:06 | DEBUG | DL-4 | Saved resource -> home/sites/composerize/static/js/main.623047e0.js +09:46:06 | INFO | MainThread | Crawl finished: 1 pages in 0.31s (0.31s avg) +10:17:29 | INFO | MainThread | [1/100] https://www.composerize.com +10:17:29 | DEBUG | MainThread | Starting new HTTPS connection (1): www.composerize.com:443 +10:17:29 | DEBUG | MainThread | https://www.composerize.com:443 "GET / HTTP/1.1" 200 867 +10:17:29 | DEBUG | MainThread | Created directory home/sites/composerize +10:17:29 | DEBUG | MainThread | Saved page home/sites/composerize/index.html +10:17:29 | DEBUG | DL-1 | Starting new HTTPS connection (2): www.composerize.com:443 +10:17:29 | DEBUG | DL-3 | Starting new HTTPS connection (3): www.composerize.com:443 +10:17:29 | DEBUG | DL-2 | Starting new HTTPS connection (4): www.composerize.com:443 +10:17:29 | DEBUG | DL-4 | https://www.composerize.com:443 "GET /favicon.ico HTTP/1.1" 200 3430 +10:17:29 | DEBUG | DL-4 | Saved resource -> home/sites/composerize/favicon.ico +10:17:29 | DEBUG | DL-3 | https://www.composerize.com:443 "GET /manifest.json HTTP/1.1" 200 288 +10:17:29 | DEBUG | DL-3 | Saved resource -> home/sites/composerize/manifest.json +10:17:29 | DEBUG | DL-2 | https://www.composerize.com:443 "GET /static/js/main.623047e0.js HTTP/1.1" 200 176652 +10:17:29 | DEBUG | DL-1 | https://www.composerize.com:443 "GET /static/css/main.757d3484.css HTTP/1.1" 200 1761 +10:17:29 | DEBUG | DL-2 | Created directory home/sites/composerize/static/js +10:17:29 | DEBUG | DL-1 | Created directory home/sites/composerize/static/css +10:17:29 | DEBUG | DL-1 | Saved resource -> home/sites/composerize/static/css/main.757d3484.css +10:17:30 | DEBUG | DL-2 | Saved resource -> home/sites/composerize/static/js/main.623047e0.js +10:17:30 | INFO | MainThread | Crawl finished: 1 pages in 0.41s (0.41s avg) +10:23:58 | INFO | MainThread | [1/100] https://www.composerize.com +10:23:58 | DEBUG | MainThread | Starting new HTTPS connection (1): www.composerize.com:443 +10:23:58 | DEBUG | MainThread | https://www.composerize.com:443 "GET / HTTP/1.1" 200 867 +10:23:58 | DEBUG | DL-3 | Starting new HTTPS connection (2): www.composerize.com:443 +10:23:58 | DEBUG | MainThread | Created directory home/sites/composerize +10:23:58 | DEBUG | MainThread | Saved page home/sites/composerize/index.html +10:23:58 | DEBUG | DL-4 | Starting new HTTPS connection (3): www.composerize.com:443 +10:23:58 | DEBUG | DL-1 | Starting new HTTPS connection (4): www.composerize.com:443 +10:23:58 | DEBUG | DL-2 | https://www.composerize.com:443 "GET /manifest.json HTTP/1.1" 200 288 +10:23:58 | DEBUG | DL-2 | Saved resource -> home/sites/composerize/manifest.json +10:23:58 | DEBUG | DL-3 | https://www.composerize.com:443 "GET /favicon.ico HTTP/1.1" 200 3430 +10:23:58 | DEBUG | DL-3 | Saved resource -> home/sites/composerize/favicon.ico +10:23:59 | DEBUG | DL-4 | https://www.composerize.com:443 "GET /static/js/main.623047e0.js HTTP/1.1" 200 176652 +10:23:59 | DEBUG | DL-4 | Created directory home/sites/composerize/static/js +10:23:59 | DEBUG | DL-1 | https://www.composerize.com:443 "GET /static/css/main.757d3484.css HTTP/1.1" 200 1761 +10:23:59 | DEBUG | DL-1 | Created directory home/sites/composerize/static/css +10:23:59 | DEBUG | DL-1 | Saved resource -> home/sites/composerize/static/css/main.757d3484.css +10:23:59 | DEBUG | DL-4 | Saved resource -> home/sites/composerize/static/js/main.623047e0.js +10:23:59 | INFO | MainThread | Crawl finished: 1 pages in 0.31s (0.31s avg) +10:27:36 | INFO | MainThread | [1/100] https://www.composerize.com +10:27:36 | DEBUG | MainThread | Starting new HTTPS connection (1): www.composerize.com:443 +10:27:36 | DEBUG | MainThread | https://www.composerize.com:443 "GET / HTTP/1.1" 200 867 +10:27:36 | DEBUG | MainThread | Created directory home/sites/composerize +10:27:36 | DEBUG | MainThread | Saved page home/sites/composerize/index.html +10:27:36 | DEBUG | DL-1 | Starting new HTTPS connection (2): www.composerize.com:443 +10:27:36 | DEBUG | DL-2 | Starting new HTTPS connection (3): www.composerize.com:443 +10:27:36 | DEBUG | DL-4 | Starting new HTTPS connection (4): www.composerize.com:443 +10:27:37 | DEBUG | DL-3 | https://www.composerize.com:443 "GET /manifest.json HTTP/1.1" 200 288 +10:27:37 | DEBUG | DL-3 | Saved resource -> home/sites/composerize/manifest.json +10:27:37 | DEBUG | DL-4 | https://www.composerize.com:443 "GET /static/css/main.757d3484.css HTTP/1.1" 200 1761 +10:27:37 | DEBUG | DL-4 | Created directory home/sites/composerize/static/css +10:27:37 | DEBUG | DL-4 | Saved resource -> home/sites/composerize/static/css/main.757d3484.css +10:27:37 | DEBUG | DL-1 | https://www.composerize.com:443 "GET /favicon.ico HTTP/1.1" 200 3430 +10:27:37 | DEBUG | DL-1 | Saved resource -> home/sites/composerize/favicon.ico +10:27:37 | DEBUG | DL-2 | https://www.composerize.com:443 "GET /static/js/main.623047e0.js HTTP/1.1" 200 176652 +10:27:37 | DEBUG | DL-2 | Created directory home/sites/composerize/static/js +10:27:37 | DEBUG | DL-2 | Saved resource -> home/sites/composerize/static/js/main.623047e0.js +10:27:37 | INFO | MainThread | Crawl finished: 1 pages in 0.39s (0.39s avg) diff --git a/website-downloader.py b/website-downloader.py new file mode 100755 index 0000000..5cbec11 --- /dev/null +++ b/website-downloader.py @@ -0,0 +1,406 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import logging +import os +import queue +import sys +import threading +import time +from hashlib import sha256 +from pathlib import Path +from typing import Optional +from urllib.parse import urljoin, urlparse + +import requests +from bs4 import BeautifulSoup +from requests.adapters import HTTPAdapter +from urllib3.util import Retry + +# --------------------------------------------------------------------------- +# Config / constants +# --------------------------------------------------------------------------- + +LOG_FMT = "%(asctime)s | %(levelname)-8s | %(threadName)s | %(message)s" + +DEFAULT_HEADERS = { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) " + "Gecko/20100101 Firefox/128.0" +} + +TIMEOUT = 15 # seconds +CHUNK_SIZE = 8192 # bytes + +# Conservative margins under common OS limits (~255–260 bytes) +MAX_PATH_LEN = 240 +MAX_SEG_LEN = 120 + + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- + +logging.basicConfig( + filename="web_scraper.log", + level=logging.DEBUG, + format=LOG_FMT, + datefmt="%H:%M:%S", + force=True, +) +_console = logging.StreamHandler(sys.stdout) +_console.setLevel(logging.INFO) +_console.setFormatter(logging.Formatter(LOG_FMT, datefmt="%H:%M:%S")) +logging.getLogger().addHandler(_console) +log = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# HTTP session (retry, timeouts, custom UA) +# --------------------------------------------------------------------------- + +SESSION = requests.Session() +RETRY_STRAT = Retry( + total=5, + backoff_factor=0.5, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET", "HEAD"], +) +SESSION.mount("http://", HTTPAdapter(max_retries=RETRY_STRAT)) +SESSION.mount("https://", HTTPAdapter(max_retries=RETRY_STRAT)) +SESSION.headers.update(DEFAULT_HEADERS) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def create_dir(path: Path) -> None: + """Create path (and parents) if it does not already exist.""" + if not path.exists(): + path.mkdir(parents=True, exist_ok=True) + log.debug("Created directory %s", path) + + +def sanitize(url_fragment: str) -> str: + """Strip back-references and Windows backslashes.""" + return url_fragment.replace("\\", "/").replace("..", "").strip() + + +NON_FETCHABLE_SCHEMES = {"mailto", "tel", "sms", "javascript", "data", "geo", "blob"} + + +def is_httpish(u: str) -> bool: + """True iff the URL is http(s) or relative (no scheme).""" + p = urlparse(u) + return (p.scheme in ("http", "https")) or (p.scheme == "") + + +def is_non_fetchable(u: str) -> bool: + """True iff the URL clearly shouldn't be fetched (mailto:, tel:, data:, ...).""" + p = urlparse(u) + return p.scheme in NON_FETCHABLE_SCHEMES + + +def is_internal(link: str, root_netloc: str) -> bool: + """Return True if link belongs to root_netloc (or is protocol-relative).""" + parsed = urlparse(link) + return not parsed.netloc or parsed.netloc == root_netloc + + +def _shorten_segment(segment: str, limit: int = MAX_SEG_LEN) -> str: + """ + Shorten a single path segment if over limit. + Preserve extension; append a short hash to keep it unique. + """ + if len(segment) <= limit: + return segment + p = Path(segment) + stem, suffix = p.stem, p.suffix + h = sha256(segment.encode("utf-8")).hexdigest()[:12] + # leave room for '-' + hash + suffix + keep = max(0, limit - len(suffix) - 13) + return f"{stem[:keep]}-{h}{suffix}" + + +def to_local_path(parsed: urlparse, site_root: Path) -> Path: + """ + Map an internal URL to a local file path under site_root. + + - Adds 'index.html' where appropriate. + - Converts extensionless paths to '.html'. + - Appends a short query-hash when ?query is present to avoid collisions. + - Enforces per-segment and overall path length limits. If still too long, + hashes the leaf name. + """ + rel = parsed.path.lstrip("/") + if not rel: + rel = "index.html" + elif rel.endswith("/"): + rel += "index.html" + elif not Path(rel).suffix: + rel += ".html" + + if parsed.query: + qh = sha256(parsed.query.encode("utf-8")).hexdigest()[:10] + p = Path(rel) + rel = str(p.with_name(f"{p.stem}-q{qh}{p.suffix}")) + + # Shorten individual segments + parts = Path(rel).parts + parts = tuple(_shorten_segment(seg, MAX_SEG_LEN) for seg in parts) + local_path = site_root / Path(*parts) + + # If full path is still too long, hash the leaf + if len(str(local_path)) > MAX_PATH_LEN: + p = local_path + h = sha256(parsed.geturl().encode("utf-8")).hexdigest()[:16] + leaf = _shorten_segment(f"{p.stem}-{h}{p.suffix}", MAX_SEG_LEN) + local_path = p.with_name(leaf) + + return local_path + + +def safe_write_text(path: Path, text: str, encoding: str = "utf-8") -> Path: + """ + Write text to path, falling back to a hashed filename if OS rejects it + (e.g., filename too long). Returns the final path used. + """ + try: + path.write_text(text, encoding=encoding) + return path + except OSError as exc: + log.warning("Write failed for %s: %s. Falling back to hashed leaf.", path, exc) + p = path + h = sha256(str(p).encode("utf-8")).hexdigest()[:16] + fallback = p.with_name(_shorten_segment(f"{p.stem}-{h}{p.suffix}", MAX_SEG_LEN)) + create_dir(fallback.parent) + fallback.write_text(text, encoding=encoding) + return fallback + + +# --------------------------------------------------------------------------- +# Fetchers +# --------------------------------------------------------------------------- + + +def fetch_html(url: str) -> Optional[BeautifulSoup]: + """Download url and return a BeautifulSoup tree (or None on error).""" + try: + resp = SESSION.get(url, timeout=TIMEOUT) + resp.raise_for_status() + return BeautifulSoup(resp.text, "html.parser") + except Exception as exc: # noqa: BLE001 + log.warning("HTTP error for %s – %s", url, exc) + return None + + +def fetch_binary(url: str, dest: Path) -> None: + """Stream url to dest unless it already exists. Safe against long paths.""" + if dest.exists(): + return + try: + resp = SESSION.get(url, timeout=TIMEOUT, stream=True) + resp.raise_for_status() + create_dir(dest.parent) + try: + with dest.open("wb") as fh: + for chunk in resp.iter_content(CHUNK_SIZE): + fh.write(chunk) + log.debug("Saved resource -> %s", dest) + except OSError as exc: + # Fallback to hashed leaf if OS rejects path + log.warning("Binary write failed for %s: %s. Using fallback.", dest, exc) + p = dest + h = sha256(str(p).encode("utf-8")).hexdigest()[:16] + fallback = p.with_name( + _shorten_segment(f"{p.stem}-{h}{p.suffix}", MAX_SEG_LEN) + ) + create_dir(fallback.parent) + with fallback.open("wb") as fh: + for chunk in resp.iter_content(CHUNK_SIZE): + fh.write(chunk) + log.debug("Saved resource (fallback) -> %s", fallback) + except Exception as exc: # noqa: BLE001 + log.error("Failed to save %s – %s", url, exc) + + +# --------------------------------------------------------------------------- +# Link rewriting +# --------------------------------------------------------------------------- + + +def rewrite_links( + soup: BeautifulSoup, page_url: str, site_root: Path, page_dir: Path +) -> None: + """Rewrite internal links to local relative paths under site_root.""" + root_netloc = urlparse(page_url).netloc + for tag in soup.find_all(["a", "img", "script", "link"]): + attr = "href" if tag.name in {"a", "link"} else "src" + if not tag.has_attr(attr): + continue + original = sanitize(tag[attr]) + if ( + original.startswith("#") + or is_non_fetchable(original) + or not is_httpish(original) + ): + continue + abs_url = urljoin(page_url, original) + if not is_internal(abs_url, root_netloc): + continue # external – leave untouched + local_path = to_local_path(urlparse(abs_url), site_root) + try: + tag[attr] = os.path.relpath(local_path, page_dir) + except ValueError: + # Different drives on Windows, etc. + tag[attr] = str(local_path) + + +# --------------------------------------------------------------------------- +# Crawl coordinator +# --------------------------------------------------------------------------- + + +def crawl_site(start_url: str, root: Path, max_pages: int, threads: int) -> None: + """Breadth-first crawl limited to max_pages. Downloads assets via workers.""" + q_pages: queue.Queue[str] = queue.Queue() + q_pages.put(start_url) + seen_pages: set[str] = set() + download_q: queue.Queue[tuple[str, Path]] = queue.Queue() + + def worker() -> None: + while True: + try: + url, dest = download_q.get(timeout=3) + except queue.Empty: + return + if is_non_fetchable(url) or not is_httpish(url): + log.debug("Skip non-fetchable: %s", url) + download_q.task_done() + continue + fetch_binary(url, dest) + download_q.task_done() + + workers: list[threading.Thread] = [] + for i in range(max(1, threads)): + t = threading.Thread(target=worker, name=f"DL-{i+1}", daemon=True) + t.start() + workers.append(t) + + start_time = time.time() + root_netloc = urlparse(start_url).netloc + + while not q_pages.empty() and len(seen_pages) < max_pages: + page_url = q_pages.get() + if page_url in seen_pages: + continue + seen_pages.add(page_url) + log.info("[%s/%s] %s", len(seen_pages), max_pages, page_url) + + soup = fetch_html(page_url) + if soup is None: + continue + + # Gather links & assets + for tag in soup.find_all(["img", "script", "link", "a"]): + link = tag.get("src") or tag.get("href") + if not link: + continue + link = sanitize(link) + if link.startswith("#") or is_non_fetchable(link) or not is_httpish(link): + continue + abs_url = urljoin(page_url, link) + parsed = urlparse(abs_url) + if not is_internal(abs_url, root_netloc): + continue + + dest_path = to_local_path(parsed, root) + # HTML? + if parsed.path.endswith("/") or not Path(parsed.path).suffix: + if abs_url not in seen_pages and abs_url not in list( + q_pages.queue + ): # type: ignore[arg-type] + q_pages.put(abs_url) + else: + download_q.put((abs_url, dest_path)) + + # Save current page + local_path = to_local_path(urlparse(page_url), root) + create_dir(local_path.parent) + rewrite_links(soup, page_url, root, local_path.parent) + html = soup.prettify() + final_path = safe_write_text(local_path, html, encoding="utf-8") + log.debug("Saved page %s", final_path) + + download_q.join() + elapsed = time.time() - start_time + if seen_pages: + log.info( + "Crawl finished: %s pages in %.2fs (%.2fs avg)", + len(seen_pages), + elapsed, + elapsed / len(seen_pages), + ) + else: + log.warning("Nothing downloaded – check URL or connectivity") + + +# --------------------------------------------------------------------------- +# Helper function for output folder +# --------------------------------------------------------------------------- + + +def make_root(url: str, custom: Optional[str]) -> Path: + """Derive output folder from URL if custom not supplied.""" + return Path(custom) if custom else Path(urlparse(url).netloc.replace(".", "_")) + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser( + description="Recursively mirror a website for offline use.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + p.add_argument( + "--url", + required=True, + help="Starting URL to crawl (e.g., https://example.com/).", + ) + p.add_argument( + "--destination", + default=None, + help="Output folder (defaults to a folder derived from the URL).", + ) + p.add_argument( + "--max-pages", + type=int, + default=50, + help="Maximum number of HTML pages to crawl.", + ) + p.add_argument( + "--threads", + type=int, + default=6, + help="Number of concurrent download workers.", + ) + return p.parse_args() + + +if __name__ == "__main__": + args = parse_args() + if args.max_pages < 1: + log.error("--max-pages must be >= 1") + sys.exit(2) + if args.threads < 1: + log.error("--threads must be >= 1") + sys.exit(2) + + host = args.url + root = make_root(args.url, args.destination) + crawl_site(host, root, args.max_pages, args.threads)