server-json/api_v1/scanphoto.js

249 lines
No EOL
7.8 KiB
JavaScript

// 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 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))
};
}
// -----------------------------------------------------
// LOGIN: ottieni e riusa il token
// -----------------------------------------------------
let cachedToken = null;
async function getToken(force = false) {
if (cachedToken && !force) return cachedToken;
try {
const res = await axios.post(`${BASE_URL}/auth/login`, { email: EMAIL, password: PASSWORD });
cachedToken = res.data.token;
return cachedToken;
} catch (err) {
console.error("ERRORE LOGIN:", err.message);
return null;
}
}
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;
}
}
// -----------------------------------------------------
// 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;