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); // directory reale: sites/ const dirPath = path.join(SITES_ROOT, dir); // URL pubblico: / app.use(`/${dir}`, express.static(dirPath)); 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}`); });