diff --git a/api_v1/scanphoto.js b/api_v1/scanphoto.js index c84b92d..583a94a 100644 --- a/api_v1/scanphoto.js +++ b/api_v1/scanphoto.js @@ -1,182 +1,249 @@ +// api_v1/scanphoto.js +require('dotenv').config(); + const fs = require('fs'); +const fsp = require('fs/promises'); const path = require('path'); +const crypto = require('crypto'); const ExifReader = require('exifreader'); const sharp = require('sharp'); const axios = require('axios'); -const loc = require('./geo.js'); -//var productsDatabase = { photos: []} -//var i = 1; -//var s = 0; -//console.log("start search"); -async function searchFile(dir, fileExt) { - var i = 1; - var s = 0; - s++; - // read the contents of the directory - console.log("è una directory?"); - if(fs.existsSync('./public/photos')){ - var ff = fs.statSync('./public/photos'); - if(!ff.isDirectory()){ - console.log("non è una dir"); - }; +const BASE_URL = process.env.BASE_URL; // es: https://prova.patachina.it/api +const EMAIL = process.env.EMAIL; +const PASSWORD = process.env.PASSWORD; + +const WEB_ROOT = 'public'; // cartella radice dei file serviti dal web server + +// Estensioni supportate (puoi ampliarle) +const SUPPORTED_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif']); + +// ----------------------------------------------------- +// UTILS +// ----------------------------------------------------- +function toPosix(p) { + // normalizza gli slash per il web (URL-friendly) + return p.split(path.sep).join('/'); +} + +function sha256(s) { + return crypto.createHash('sha256').update(s).digest('hex'); +} + +function inferMimeFromExt(ext) { + switch (ext.toLowerCase()) { + case '.jpg': + case '.jpeg': return 'image/jpeg'; + case '.png': return 'image/png'; + case '.webp': return 'image/webp'; + case '.heic': + case '.heif': return 'image/heic'; + default: return 'application/octet-stream'; + } +} + +// EXIF "YYYY:MM:DD HH:mm:ss" -> ISO-8601 UTC +function parseExifDateUtc(s) { + if (!s) return null; + const re = /^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/; + const m = re.exec(s); + if (!m) return null; + const dt = new Date(Date.UTC(+m[1], +m[2]-1, +m[3], +m[4], +m[5], +m[6])); + return dt.toISOString(); // es. "2017-08-19T11:30:14.000Z" +} + +function mapGps(gps) { + if (!gps) return null; + const lat = gps.Latitude; + const lng = gps.Longitude; + const alt = gps.Altitude; + if (lat == null || lng == null) return null; + return { + lat: typeof lat === 'number' ? lat : Number(lat), + lng: typeof lng === 'number' ? lng : Number(lng), + alt: (alt == null) ? null : (typeof alt === 'number' ? alt : Number(alt)) }; - const files = fs.readdirSync(dir); - // search through the files - for (let k = 0; k < files.length; k++) { - file = files[k]; - const filePath = path.join(dir, file); - - // get the file stats - const fileStat = fs.statSync(filePath); - // if the file is a directory, recursively search the directory - if (fileStat.isDirectory()) { - await searchFile(filePath, fileExt); - } else if (path.extname(file).toLowerCase() == fileExt) { - // if the file is a match, print it - var dd = dir.split("/"); - var d = dd.slice(1); - var d1 = dd.slice(1); - d1[1]= "thumbs"; - var ff = file.split("."); - var fn = ff.slice(0,-1).join("."); - var f1 = fn+'_min'+'.'+ff.at(-1); - var f2 = fn+'_avg'+'.'+ff.at(-1); - var f3 = fn+'_sharp'+'.'+ff.at(-1); - var extFilePath = path.join(d.join("/"),file); - var extThumbMinPath = path.join(d1.join("/"),f1); - var extThumbAvgPath = path.join(d1.join("/"),f2); - var extThumbSharpPath = path.join("public",d1.join("/"),f3); - var thumbMinPath = path.join("public",extThumbMinPath); - var thumbAvgPath = path.join("public",extThumbAvgPath); - var dt = path.join("public",d1.join("/")); - if (!fs.existsSync(dt)){ - fs.mkdirSync(dt, { recursive: true }); - } - var data; - try { - data = fs.readFileSync(filePath); - } catch (err) { - //console.error(err); - } - const tags = await ExifReader.load(filePath, {expanded: true}); - //console.log(tags); - var time = tags.exif.DateTimeOriginal; - if (time === undefined){} else {time=time.value[0]} - var gps = tags['gps']; - //console.log(gps.Latitude); - //console.log(gps.latitude); - //var loc; - var locat; - //console.log("ora"); - if (gps === undefined){} else { - // locat = await loc(gps.Longitude,gps.Latitude); - } - //if (time === undefined ){console.log(filePath)} - //console.log("read: "+filePath); - await sharp(data) - .resize(100,100,"inside") - .withMetadata() - .toFile(thumbMinPath, (err, info) => {}); - await sharp(data) - .resize(400) - .withMetadata() - .toFile(thumbAvgPath, (err, info) => {}); - console.log(i+" - "+file); - scrivi({ - id: i, - name: file, - path: extFilePath, - thub1: extThumbMinPath, - thub2: extThumbAvgPath, - gps: tags['gps'], - data: time, - location: locat - }); - i++; - } - if(k == files.length-1) { - if (s == 1) { - //scrivi(productsDatabase.photos); - //return; - //console.log("finito1"); - //console.log(productsDatabase); - } else { - s--; - } - - } - } - } -async function thumb(filePath, opt){ +// ----------------------------------------------------- +// LOGIN: ottieni e riusa il token +// ----------------------------------------------------- +let cachedToken = null; + +async function getToken(force = false) { + if (cachedToken && !force) return cachedToken; try { - const thumbnail = await imageThumbnail(filePath, opt); - //console.log(thumbnail); - return thumbnail; + const res = await axios.post(`${BASE_URL}/auth/login`, { email: EMAIL, password: PASSWORD }); + cachedToken = res.data.token; + return cachedToken; } catch (err) { - //console.error(err); + console.error("ERRORE LOGIN:", err.message); + return null; } -} -// start the search in the current directory -async function scanPhoto(dir){ - await searchFile(dir, '.jpg'); - //console.log("finito2"); } -function scrivi(json) { - fetch('http://192.168.1.3:7771/auth/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({'email':'fabio@gmail.com', 'password':'master66'}), - }) - .then(response => response.json()) - .then(user1 => { - const myHeaders = new Headers(); - myHeaders.append('Authorization', 'Bearer ' + user1.token); - myHeaders.append('Content-Type', 'application/json'); - //console.log(myHeaders.get("Content-Type")); - //console.log(myHeaders.get("Authorization")); - fetch('http://192.168.1.3:7771/photos', { - method: 'POST', - headers: myHeaders, - body: JSON.stringify(json), - }) - .then(response => response.json()) - //.then(user => console.log("caricato")); - +async function postWithAuth(url, payload) { + let token = await getToken(); + if (!token) throw new Error('Token assente'); + + try { + return await axios.post(url, payload, { + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" } }); + } catch (err) { + // Se scaduto, prova una volta a rinnovare + if (err.response && err.response.status === 401) { + token = await getToken(true); + if (!token) throw err; + return await axios.post(url, payload, { + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" } + }); + } + throw err; + } } -function azzera() { - fetch('http://192.168.1.3:7771/auth/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({'email':'fabio@gmail.com', 'password':'master66'}), - }) - .then(response => response.json()) - .then(user1 => { - const myHeaders = new Headers(); - myHeaders.append('Authorization', 'Bearer ' + user1.token); - myHeaders.append('Content-Type', 'application/json'); - //console.log(myHeaders.get("Content-Type")); - //console.log(myHeaders.get("Authorization")); - fetch('http://192.168.1.3:7771/photos', { - method: 'POST', - headers: myHeaders, - body: "", - }) - .then(response => response.json()) - .then(user => console.log("azzerato totalmente")); - +// ----------------------------------------------------- +// INVIA FOTO AL SERVER +// ----------------------------------------------------- +async function sendPhoto(json) { + try { + await postWithAuth(`${BASE_URL}/photos`, json); + } catch (err) { + console.error("Errore invio foto:", err.message); + } +} + +// ----------------------------------------------------- +// CREA THUMBNAILS (min: 100px lato lungo, avg: 400px lato lungo) +// ----------------------------------------------------- +async function createThumbnails(filePath, thumbMinPath, thumbAvgPath) { + try { + await sharp(filePath) + .resize({ width: 100, height: 100, fit: "inside", withoutEnlargement: true }) + .withMetadata() + .toFile(thumbMinPath); + + await sharp(filePath) + .resize({ width: 400, withoutEnlargement: true }) + .withMetadata() + .toFile(thumbAvgPath); + } catch (err) { + console.error("Errore creazione thumbnails:", err.message, filePath); + } +} + +// ----------------------------------------------------- +// SCANSIONE RICORSIVA (asincrona, con metadati extra) +// ----------------------------------------------------- +async function scanDir(dir, results = []) { + const files = await fsp.readdir(dir, { withFileTypes: true }); + + for (const dirent of files) { + const filePath = path.join(dir, dirent.name); + + if (dirent.isDirectory()) { + await scanDir(filePath, results); + continue; + } + + const ext = path.extname(dirent.name).toLowerCase(); + if (!SUPPORTED_EXTS.has(ext)) continue; + + console.log("Trovato:", dirent.name); + + // Directory relativa rispetto a WEB_ROOT (es. "photos/original/...") + const relDir = toPosix(path.relative(WEB_ROOT, dir)); + const relFile = toPosix(path.join(relDir, dirent.name)); // path relativo (web-safe) + + // Cartella thumbs parallela a original + const thumbDirAbs = path.join(WEB_ROOT, relDir.replace(/original/i, 'thumbs')); + await fsp.mkdir(thumbDirAbs, { recursive: true }); + + const baseName = path.parse(dirent.name).name; + const extName = path.parse(dirent.name).ext; + + // Path ASSOLUTI dei file thumb sul filesystem + const thumbMinAbs = path.join(thumbDirAbs, `${baseName}_min${extName}`); + const thumbAvgAbs = path.join(thumbDirAbs, `${baseName}_avg${extName}`); + + await createThumbnails(filePath, thumbMinAbs, thumbAvgAbs); + + // Path RELATIVI (web) per il JSON + const thumbMinRel = toPosix(path.relative(WEB_ROOT, thumbMinAbs)); // es. "photos/thumbs/..._min.jpg" + const thumbAvgRel = toPosix(path.relative(WEB_ROOT, thumbAvgAbs)); + + // EXIF (expanded: true per avere gps strutturato come nel tuo attuale) + let tags = {}; + try { + tags = await ExifReader.load(filePath, { expanded: true }); + } catch (err) { + // nessun EXIF, ok + } + const timeRaw = tags?.exif?.DateTimeOriginal?.value?.[0] || null; + const isoUtc = parseExifDateUtc(timeRaw); + const gps = mapGps(tags?.gps); + + // Metadati immagine (width/height/size_bytes, mime) + let width = null, height = null, size_bytes = null; + try { + const meta = await sharp(filePath).metadata(); + width = meta.width || null; + height = meta.height || null; + const st = await fsp.stat(filePath); + size_bytes = st.size; + } catch (err) { + console.warn("Impossibile leggere metadata immagine:", err.message); + try { + const st = await fsp.stat(filePath); + size_bytes = st.size; + } catch {} + } + const mime_type = inferMimeFromExt(ext); + + // ID stabile (se non vuoi usare quello del DB lato server) + const stableId = sha256(relFile); + + results.push({ + // campi "nuovi"/normalizzati + id: stableId, // oppure lascia che lo generi il DB e rimuovi questo campo + name: dirent.name, + path: relFile, // <== RELATIVO, web-safe (non contiene dominio) + thub1: thumbMinRel, // <== RELATIVO + thub2: thumbAvgRel, // <== RELATIVO + gps, // {lat,lng,alt} oppure null + data: timeRaw, // EXIF originale (se vuoi mantenerlo) + taken_at: isoUtc, // ISO-8601 UTC (comodo per indicizzazione lato app) + mime_type, + width, + height, + size_bytes, + location: null }); + } + + return results; } +// ----------------------------------------------------- +// FUNZIONE PRINCIPALE +// ----------------------------------------------------- +async function scanPhoto(dir) { + console.log("Inizio scansione:", dir); + + const photos = await scanDir(dir); + + console.log("Trovate", photos.length, "foto"); + + // invio seriale; se vuoi parallelizzare, limita la concorrenza (es. p-limit) + for (const p of photos) { + await sendPhoto(p); + } + + console.log("Scansione completata"); +} + +module.exports = scanPhoto; module.exports = scanPhoto; \ No newline at end of file