multi_static_website/server.js
2025-12-23 13:06:25 +01:00

330 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/<site>
const dirPath = path.join(SITES_ROOT, site.dir);
// URL pubblico: /<site>
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 linput 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}`);
});