Aggiorna api_v1/scanphoto.js
This commit is contained in:
parent
242256dba6
commit
71fce6db36
1 changed files with 226 additions and 159 deletions
|
|
@ -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;
|
||||
Loading…
Reference in a new issue