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