first commit
6
.env
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
BASE_URL=https://prova.patachina.it
|
||||
SERVER_PORT=4000
|
||||
EMAIL=fabio@gmail.com
|
||||
PASSWORD=master66
|
||||
JWT_SECRET=123456789
|
||||
JWT_EXPIRES=1h
|
||||
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules/
|
||||
25
api_v1/altri/generateData.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Required libraries
|
||||
*/
|
||||
const faker = require('faker')
|
||||
|
||||
var productsDatabase = { products: [], sellers: [] }
|
||||
|
||||
for (var i = 1; i <= 10; i++) {
|
||||
productsDatabase.products.push({
|
||||
id: i,
|
||||
name: faker.commerce.product(),
|
||||
color: faker.commerce.color(),
|
||||
cost: faker.commerce.price(),
|
||||
quantity: Math.floor(Math.random() * 1000)
|
||||
})
|
||||
productsDatabase.sellers.push({
|
||||
id: i,
|
||||
firstName: faker.name.firstName(),
|
||||
lastName: faker.name.lastName(),
|
||||
jobArea: faker.name.jobArea(),
|
||||
address: faker.address.country()
|
||||
})
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(productsDatabase))
|
||||
108
api_v1/altri/photo.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const ExifReader = require('exifreader');
|
||||
const sharp = require('sharp');
|
||||
|
||||
var productsDatabase = { photos: []}
|
||||
var i = 1;
|
||||
var s = 0;
|
||||
//console.log("start search");
|
||||
async function searchFile(dir, fileExt) {
|
||||
s++;
|
||||
// read the contents of the directory
|
||||
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'];
|
||||
//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(pub);
|
||||
productsDatabase.photos.push({
|
||||
id: i,
|
||||
name: file,
|
||||
path: extFilePath,
|
||||
thub1: extThumbMinPath,
|
||||
thub2: extThumbAvgPath,
|
||||
gps: tags['gps'],
|
||||
data: time
|
||||
});
|
||||
i++;
|
||||
}
|
||||
if(k == files.length-1) {
|
||||
if (s == 1) {
|
||||
fs.writeFileSync("api_v1/db.json", JSON.stringify(productsDatabase));
|
||||
//console.log("finito");
|
||||
//console.log(productsDatabase);
|
||||
} else {
|
||||
s--;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function thumb(filePath, opt){
|
||||
try {
|
||||
const thumbnail = await imageThumbnail(filePath, opt);
|
||||
//console.log(thumbnail);
|
||||
return thumbnail;
|
||||
} catch (err) {
|
||||
//console.error(err);
|
||||
}
|
||||
}
|
||||
// start the search in the current directory
|
||||
async function run(){
|
||||
await searchFile('./public/photos/original', '.jpg');
|
||||
//console.log(JSON.stringify(productsDatabase))
|
||||
}
|
||||
|
||||
run()
|
||||
108
api_v1/altri/photo1.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const ExifReader = require('exifreader');
|
||||
const sharp = require('sharp');
|
||||
|
||||
var productsDatabase = { photos: []}
|
||||
var i = 1;
|
||||
var s = 0;
|
||||
//console.log("start search");
|
||||
async function searchFile(dir, fileExt) {
|
||||
s++;
|
||||
// read the contents of the directory
|
||||
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'];
|
||||
//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);
|
||||
productsDatabase.photos.push({
|
||||
id: i,
|
||||
name: file,
|
||||
path: extFilePath,
|
||||
thub1: extThumbMinPath,
|
||||
thub2: extThumbAvgPath,
|
||||
gps: tags['gps'],
|
||||
data: time
|
||||
});
|
||||
i++;
|
||||
}
|
||||
if(k == files.length-1) {
|
||||
if (s == 1) {
|
||||
fs.writeFileSync("api_v1/db.json", JSON.stringify(productsDatabase));
|
||||
//console.log("finito");
|
||||
//console.log(productsDatabase);
|
||||
} else {
|
||||
s--;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function thumb(filePath, opt){
|
||||
try {
|
||||
const thumbnail = await imageThumbnail(filePath, opt);
|
||||
//console.log(thumbnail);
|
||||
return thumbnail;
|
||||
} catch (err) {
|
||||
//console.error(err);
|
||||
}
|
||||
}
|
||||
// start the search in the current directory
|
||||
async function run(){
|
||||
await searchFile('./public/photos/original', '.jpg');
|
||||
//console.log(JSON.stringify(productsDatabase))
|
||||
}
|
||||
|
||||
run()
|
||||
173
api_v1/altri/scanphoto1.js
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
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) {
|
||||
//azzera();
|
||||
s++;
|
||||
// read the contents of the directory
|
||||
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);
|
||||
productsDatabase.photos.push({
|
||||
id: i,
|
||||
name: file,
|
||||
path: extFilePath,
|
||||
thub1: extThumbMinPath,
|
||||
thub2: extThumbAvgPath,
|
||||
gps: tags['gps'],
|
||||
data: time
|
||||
});
|
||||
i++;
|
||||
}
|
||||
if(k == files.length-1) {
|
||||
if (s == 1) {
|
||||
fs.writeFileSync('api_v1/db.json', JSON.stringify(productsDatabase));
|
||||
//console.log("finito1");
|
||||
//console.log(productsDatabase);
|
||||
} else {
|
||||
s--;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function thumb(filePath, opt){
|
||||
try {
|
||||
const thumbnail = await imageThumbnail(filePath, opt);
|
||||
//console.log(thumbnail);
|
||||
return thumbnail;
|
||||
} catch (err) {
|
||||
//console.error(err);
|
||||
}
|
||||
}
|
||||
// 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"));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
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"));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = scanPhoto;
|
||||
2907
api_v1/db.json
Normal file
96
api_v1/geo.js
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
const axios = require("axios");
|
||||
|
||||
// Funzione principale
|
||||
async function loc(lng, lat) {
|
||||
const primary = await place(lng, lat); // Geoapify
|
||||
const fallback = await placePhoton(lng, lat); // Photon
|
||||
|
||||
// Se Geoapify fallisce → usa Photon
|
||||
if (!primary) return fallback;
|
||||
|
||||
// Se Geoapify manca city → prendi da Photon
|
||||
if (!primary.city && fallback?.city) {
|
||||
primary.city = fallback.city;
|
||||
}
|
||||
|
||||
// Se Geoapify manca postcode → prendi da Photon
|
||||
if (!primary.postcode && fallback?.postcode) {
|
||||
primary.postcode = fallback.postcode;
|
||||
}
|
||||
|
||||
// Se Geoapify manca address → prendi da Photon
|
||||
if (!primary.address && fallback?.address) {
|
||||
primary.address = fallback.address;
|
||||
}
|
||||
|
||||
// Se Geoapify manca region → prendi da Photon
|
||||
if (!primary.region && fallback?.region) {
|
||||
primary.region = fallback.region;
|
||||
}
|
||||
|
||||
// Se Geoapify manca county_code → Photon NON lo fornisce
|
||||
// quindi non possiamo riempirlo
|
||||
|
||||
return primary;
|
||||
}
|
||||
|
||||
// Geoapify (sorgente principale)
|
||||
async function place(lng, lat) {
|
||||
const apiKey = "6dc7fb95a3b246cfa0f3bcef5ce9ed9a";
|
||||
const url = `https://api.geoapify.com/v1/geocode/reverse?lat=${lat}&lon=${lng}&apiKey=${apiKey}`;
|
||||
|
||||
try {
|
||||
const r = await axios.get(url);
|
||||
|
||||
if (r.status !== 200) return undefined;
|
||||
if (!r.data.features || r.data.features.length === 0) return undefined;
|
||||
|
||||
const k = r.data.features[0].properties;
|
||||
|
||||
return {
|
||||
continent: k?.timezone?.name?.split("/")?.[0] || undefined,
|
||||
country: k?.country || undefined,
|
||||
region: k?.state || undefined,
|
||||
postcode: k?.postcode || undefined,
|
||||
city: k?.city || undefined,
|
||||
county_code: k?.county_code || undefined,
|
||||
address: k?.address_line1 || undefined,
|
||||
timezone: k?.timezone?.name || undefined,
|
||||
time: k?.timezone?.offset_STD || undefined
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Photon (fallback)
|
||||
async function placePhoton(lng, lat) {
|
||||
try {
|
||||
const url = `https://photon.patachina.it/reverse?lon=${lng}&lat=${lat}`;
|
||||
const r = await axios.get(url);
|
||||
|
||||
if (!r.data || !r.data.features || r.data.features.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const p = r.data.features[0].properties;
|
||||
|
||||
return {
|
||||
continent: undefined, // Photon non lo fornisce
|
||||
country: p.country || undefined,
|
||||
region: p.state || undefined,
|
||||
postcode: p.postcode || undefined,
|
||||
city: p.city || p.town || p.village || undefined,
|
||||
county_code: undefined, // Photon non fornisce codici ISO
|
||||
address: p.street ? `${p.street} ${p.housenumber || ""}`.trim() : undefined,
|
||||
timezone: undefined,
|
||||
time: undefined
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = loc;
|
||||
55
api_v1/geo.js.old
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
|
||||
const axios = require('axios');
|
||||
|
||||
|
||||
|
||||
|
||||
async function loc(lng,lat){
|
||||
const l = await place(lng,lat);
|
||||
if(l === undefined){
|
||||
return await place1(lng,lat);
|
||||
} else {
|
||||
if(l.county_code === undefined){
|
||||
const l1 = await place1(lng,lat);
|
||||
if(l1 === undefined){ } else {
|
||||
l.county_code = l1.province_code;
|
||||
}
|
||||
}
|
||||
return l;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function place(lng,lat) {
|
||||
const myAPIKey = "6dc7fb95a3b246cfa0f3bcef5ce9ed9a";
|
||||
const reverseGeocodingUrl = `https://api.geoapify.com/v1/geocode/reverse?lat=${lat}&lon=${lng}&apiKey=${myAPIKey}`
|
||||
//console.log(reverseGeocodingUrl);
|
||||
try {
|
||||
const f = await axios.get(reverseGeocodingUrl);
|
||||
if(f.status == 200){
|
||||
const k = f.data.features[0].properties;
|
||||
return {continent: k.timezone.name.split("/")[0],country: k.country, region: k.state, postcode: k.postcode, city: k.city, county_code: k.county_code, address: k.address_line1, timezone: k.timezone.name, time: k.timezone.offset_STD }
|
||||
} else {
|
||||
//console.log("errore1a");
|
||||
return undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
//console.log("errore1b");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function place1(lng,lat){
|
||||
try {
|
||||
const loc = await axios.get('http://192.168.1.3:6565/query?'+lng+'&'+lat);
|
||||
const k = loc.data
|
||||
//console.log(loc);
|
||||
return {continent: k.region,country: k.country, region: undefined, postcode: undefined, city: undefined, county_code: k.province_code.split("-")[1], address: undefined, timezone: undefined, time: undefined };
|
||||
} catch (error) {
|
||||
//console.log("errore2");
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = loc;
|
||||
75
api_v1/geo.js.old2
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
const axios = require('axios');
|
||||
|
||||
async function loc(lng, lat) {
|
||||
const primary = await place(lng, lat);
|
||||
|
||||
if (!primary) {
|
||||
return await place1(lng, lat);
|
||||
}
|
||||
|
||||
if (!primary.county_code) {
|
||||
const fallback = await place1(lng, lat);
|
||||
if (fallback && fallback.county_code) {
|
||||
primary.county_code = fallback.county_code;
|
||||
}
|
||||
}
|
||||
|
||||
return primary;
|
||||
}
|
||||
|
||||
async function place(lng, lat) {
|
||||
const apiKey = "6dc7fb95a3b246cfa0f3bcef5ce9ed9a";
|
||||
const url = `https://api.geoapify.com/v1/geocode/reverse?lat=${lat}&lon=${lng}&apiKey=${apiKey}`;
|
||||
|
||||
try {
|
||||
const r = await axios.get(url);
|
||||
|
||||
if (r.status !== 200) return undefined;
|
||||
if (!r.data.features || r.data.features.length === 0) return undefined;
|
||||
|
||||
const k = r.data.features[0].properties;
|
||||
|
||||
return {
|
||||
continent: k?.timezone?.name?.split("/")?.[0] || undefined,
|
||||
country: k?.country || undefined,
|
||||
region: k?.state || undefined,
|
||||
postcode: k?.postcode || undefined,
|
||||
city: k?.city || undefined,
|
||||
county_code: k?.county_code || undefined,
|
||||
address: k?.address_line1 || undefined,
|
||||
timezone: k?.timezone?.name || undefined,
|
||||
time: k?.timezone?.offset_STD || undefined
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function place1(lng, lat) {
|
||||
try {
|
||||
const r = await axios.get(`http://192.168.1.3:6565/query?${lng}&${lat}`);
|
||||
const k = r.data;
|
||||
|
||||
const county = k?.province_code?.includes("-")
|
||||
? k.province_code.split("-")[1]
|
||||
: k?.province_code;
|
||||
|
||||
return {
|
||||
continent: k?.region || undefined,
|
||||
country: k?.country || undefined,
|
||||
region: undefined,
|
||||
postcode: undefined,
|
||||
city: undefined,
|
||||
county_code: county || undefined,
|
||||
address: undefined,
|
||||
timezone: undefined,
|
||||
time: undefined
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = loc;
|
||||
15
api_v1/geo/georev.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
const axios = require('axios');
|
||||
|
||||
axios.get('http://192.168.1.3:6565/query?12.082991&44.250747')
|
||||
.then(res => {
|
||||
const headerDate = res.headers && res.headers.date ? res.headers.date : 'no response date';
|
||||
console.log('Status Code:', res.status);
|
||||
console.log('Date in Response header:', headerDate);
|
||||
|
||||
console.log(res.data);
|
||||
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('Error: ', err.message);
|
||||
});
|
||||
16
api_v1/geo/georev1.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
const axios = require('axios');
|
||||
|
||||
|
||||
console.log("richiesta loc");
|
||||
|
||||
run();
|
||||
async function run(){
|
||||
try {
|
||||
loc = await axios.get('http://192.168.1.3:6565/query?-9.437794444444444&52.96421388888889');
|
||||
console.log(loc.data);
|
||||
} catch (error) {
|
||||
loc = "errore";// Handle errors
|
||||
console.log(loc);
|
||||
}
|
||||
}
|
||||
60
api_v1/geo/georev2.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
|
||||
const axios = require('axios');
|
||||
|
||||
|
||||
|
||||
run();
|
||||
|
||||
async function run(){
|
||||
var a = await loc(12.082978,44.250746);
|
||||
console.log(a);
|
||||
}
|
||||
|
||||
|
||||
async function loc(lng,lat){
|
||||
const l = await place(lng,lat);
|
||||
if(l === undefined){
|
||||
return await place1(lng,lat);
|
||||
} else {
|
||||
if(l.county_code === undefined){
|
||||
const l1 = await place1(lng,lat);
|
||||
if(l1 === undefined){ } else {
|
||||
l.county_code = l1.province_code.split("-")[1];
|
||||
}
|
||||
}
|
||||
return l;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function place(lng,lat) {
|
||||
const myAPIKey = "6dc7fb95a3b246cfa0f3bcef5ce9ed9a";
|
||||
const reverseGeocodingUrl = `https://api.geoapify1.com/v1/geocode/reverse?lat=${lat}&lon=${lng}&apiKey=${myAPIKey}`
|
||||
//console.log(reverseGeocodingUrl);
|
||||
try {
|
||||
const f = await axios.get(reverseGeocodingUrl);
|
||||
if(f.status == 200){
|
||||
const k = f.data.features[0].properties;
|
||||
return {continent: k.timezone.name.split("/")[0],country: k.country, region: k.state, postcode: k.postcode, city: k.city, county_code: k.county_code, address: k.address_line1, timezone: k.timezone.name, time: k.timezone.offset_STD }
|
||||
} else {
|
||||
//console.log("errore1a");
|
||||
return undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
//console.log("errore1b");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function place1(lng,lat){
|
||||
try {
|
||||
const loc = await axios.get('http://192.168.1.3:6565/query?'+lng+'&'+lat);
|
||||
const k = loc.data
|
||||
//console.log(loc);
|
||||
return {continent: k.region,country: k.country, region: undefined, postcode: undefined, city: undefined, county_code: k.province_code.split("-")[1], address: undefined, timezone: undefined, time: undefined };
|
||||
} catch (error) {
|
||||
//console.log("errore2");
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
39
api_v1/geo/georev3.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
|
||||
const axios = require('axios');
|
||||
|
||||
|
||||
|
||||
run();
|
||||
|
||||
async function run(){
|
||||
var a = await loc(12.082978,44.250746);
|
||||
console.log(a);
|
||||
}
|
||||
|
||||
|
||||
async function loc(lng,lat){
|
||||
const l = await place(lng,lat);
|
||||
return l;
|
||||
}
|
||||
|
||||
|
||||
async function place(lng,lat) {
|
||||
const reverseGeocodingUrl = 'https://nominatim.openstreetmap.org/reverse?lat='+lat+'&lon='+lng+'&format=jsonv2';
|
||||
//console.log(reverseGeocodingUrl);
|
||||
try {
|
||||
const f = await axios.get(reverseGeocodingUrl);
|
||||
if(f.status == 200){
|
||||
const k = f.data.address;
|
||||
//return {continent: k.timezone.name.split("/")[0],country: k.country, region: k.state, postcode: k.postcode, city: k.city, county_code: k.county_code, address: k.address_line1, timezone: k.timezone.name, time: k.timezone.offset_STD }
|
||||
return k;
|
||||
} else {
|
||||
//console.log("errore1a");
|
||||
return undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
//console.log("errore1b");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
13
api_v1/index mmm.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script>
|
||||
window.addEventListener("load", (event) => {
|
||||
window.location.href = "pub/index.html";
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
api_v1/initialDB.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"photos":[]}
|
||||
380
api_v1/scanphoto.js
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
/* eslint-disable no-console */
|
||||
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 { exec } = require('child_process');
|
||||
|
||||
// IMPORT GEO.JS
|
||||
const loc = require('./geo.js');
|
||||
|
||||
const BASE_URL = process.env.BASE_URL;
|
||||
const EMAIL = process.env.EMAIL;
|
||||
const PASSWORD = process.env.PASSWORD;
|
||||
|
||||
const SEND_PHOTOS = (process.env.SEND_PHOTOS || 'true').toLowerCase() === 'true';
|
||||
const WRITE_INDEX = (process.env.WRITE_INDEX || 'true').toLowerCase() === 'true';
|
||||
const WEB_ROOT = process.env.WEB_ROOT || 'public';
|
||||
const INDEX_PATH = process.env.INDEX_PATH || path.posix.join('photos', 'index.json');
|
||||
|
||||
const SUPPORTED_EXTS = new Set([
|
||||
'.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif',
|
||||
'.mp4', '.mov', '.m4v'
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// UTILS
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function toPosix(p) {
|
||||
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';
|
||||
case '.mp4': return 'video/mp4';
|
||||
case '.mov': return 'video/quicktime';
|
||||
case '.m4v': return 'video/x-m4v';
|
||||
default: return 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// GPS — FOTO
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function extractGpsFromExif(tags) {
|
||||
if (!tags?.gps) return null;
|
||||
|
||||
const lat = tags.gps.Latitude;
|
||||
const lng = tags.gps.Longitude;
|
||||
const alt = tags.gps.Altitude;
|
||||
|
||||
if (lat == null || lng == null) return null;
|
||||
|
||||
return {
|
||||
lat: Number(lat),
|
||||
lng: Number(lng),
|
||||
alt: alt != null ? Number(alt) : null
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// GPS — VIDEO (exiftool)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function extractGpsWithExiftool(videoPath) {
|
||||
return new Promise((resolve) => {
|
||||
const cmd = `exiftool -n -G1 -a -gps:all -quicktime:all -user:all "${videoPath}"`;
|
||||
exec(cmd, (err, stdout) => {
|
||||
|
||||
if (err || !stdout) return resolve(null);
|
||||
|
||||
const userData = stdout.match(/GPS Coordinates\s*:\s*([0-9\.\-]+)\s+([0-9\.\-]+)/i);
|
||||
if (userData) {
|
||||
return resolve({
|
||||
lat: Number(userData[1]),
|
||||
lng: Number(userData[2]),
|
||||
alt: null
|
||||
});
|
||||
}
|
||||
|
||||
const lat1 = stdout.match(/GPSLatitude\s*:\s*([0-9\.\-]+)/i);
|
||||
const lng1 = stdout.match(/GPSLongitude\s*:\s*([0-9\.\-]+)/i);
|
||||
if (lat1 && lng1) {
|
||||
return resolve({
|
||||
lat: Number(lat1[1]),
|
||||
lng: Number(lng1[1]),
|
||||
alt: null
|
||||
});
|
||||
}
|
||||
|
||||
const coords = stdout.match(/GPSCoordinates\s*:\s*([0-9\.\-]+)\s+([0-9\.\-]+)/i);
|
||||
if (coords) {
|
||||
return resolve({
|
||||
lat: Number(coords[1]),
|
||||
lng: Number(coords[2]),
|
||||
alt: null
|
||||
});
|
||||
}
|
||||
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// AUTH / POST
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let cachedToken = null;
|
||||
|
||||
async function getToken(force = false) {
|
||||
if (!SEND_PHOTOS) return null;
|
||||
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) {
|
||||
if (!SEND_PHOTOS) return;
|
||||
|
||||
let token = await getToken();
|
||||
if (!token) throw new Error('Token assente');
|
||||
|
||||
try {
|
||||
await axios.post(url, payload, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 20000,
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
token = await getToken(true);
|
||||
if (!token) throw err;
|
||||
|
||||
await axios.post(url, payload, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 20000,
|
||||
});
|
||||
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// VIDEO: ffmpeg thumbnail
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function createVideoThumbnail(videoPath, thumbMinPath, thumbAvgPath) {
|
||||
return new Promise((resolve) => {
|
||||
const cmd = `
|
||||
ffmpeg -y -i "${videoPath}" -ss 00:00:01.000 -vframes 1 "${thumbAvgPath}" &&
|
||||
ffmpeg -y -i "${thumbAvgPath}" -vf "scale=100:-1" "${thumbMinPath}"
|
||||
`;
|
||||
|
||||
exec(cmd, () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// VIDEO: ffprobe metadata
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function probeVideo(videoPath) {
|
||||
return new Promise((resolve) => {
|
||||
const cmd = `ffprobe -v quiet -print_format json -show_format -show_streams "${videoPath}"`;
|
||||
exec(cmd, (err, stdout) => {
|
||||
if (err) return resolve({});
|
||||
try {
|
||||
resolve(JSON.parse(stdout));
|
||||
} catch {
|
||||
resolve({});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// THUMBNAILS IMMAGINI
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
async function scanDir(dirAbs, results = []) {
|
||||
const dirEntries = await fsp.readdir(dirAbs, { withFileTypes: true });
|
||||
|
||||
for (const dirent of dirEntries) {
|
||||
const absPath = path.join(dirAbs, dirent.name);
|
||||
|
||||
if (dirent.isDirectory()) {
|
||||
await scanDir(absPath, results);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ext = path.extname(dirent.name).toLowerCase();
|
||||
if (!SUPPORTED_EXTS.has(ext)) continue;
|
||||
|
||||
const isVideo = ['.mp4', '.mov', '.m4v'].includes(ext);
|
||||
|
||||
const relFile = toPosix(path.relative(WEB_ROOT, absPath));
|
||||
const relDir = toPosix(path.posix.dirname(relFile));
|
||||
|
||||
const relThumbDir = relDir.replace(/original/i, 'thumbs');
|
||||
const absThumbDir = path.join(WEB_ROOT, relThumbDir);
|
||||
await fsp.mkdir(absThumbDir, { recursive: true });
|
||||
|
||||
const baseName = path.parse(dirent.name).name;
|
||||
|
||||
const absThumbMin = path.join(absThumbDir, `${baseName}_min.jpg`);
|
||||
const absThumbAvg = path.join(absThumbDir, `${baseName}_avg.jpg`);
|
||||
|
||||
if (isVideo) {
|
||||
await createVideoThumbnail(absPath, absThumbMin, absThumbAvg);
|
||||
} else {
|
||||
await createThumbnails(absPath, absThumbMin, absThumbAvg);
|
||||
}
|
||||
|
||||
const relThumbMin = toPosix(path.relative(WEB_ROOT, absThumbMin));
|
||||
const relThumbAvg = toPosix(path.relative(WEB_ROOT, absThumbAvg));
|
||||
|
||||
let tags = {};
|
||||
try {
|
||||
tags = await ExifReader.load(absPath, { expanded: true });
|
||||
} catch {}
|
||||
|
||||
const timeRaw = tags?.exif?.DateTimeOriginal?.value?.[0] || null;
|
||||
const takenAtIso = parseExifDateUtc(timeRaw);
|
||||
|
||||
let gps = null;
|
||||
|
||||
if (isVideo) {
|
||||
gps = await extractGpsWithExiftool(absPath);
|
||||
} else {
|
||||
gps = extractGpsFromExif(tags);
|
||||
}
|
||||
|
||||
let width = null, height = null, size_bytes = null, duration = null;
|
||||
|
||||
const st = await fsp.stat(absPath);
|
||||
size_bytes = st.size;
|
||||
|
||||
if (isVideo) {
|
||||
const info = await probeVideo(absPath);
|
||||
const stream = info.streams?.find(s => s.width && s.height);
|
||||
if (stream) {
|
||||
width = stream.width;
|
||||
height = stream.height;
|
||||
}
|
||||
duration = info.format?.duration || null;
|
||||
} else {
|
||||
try {
|
||||
const meta = await sharp(absPath).metadata();
|
||||
width = meta.width || null;
|
||||
height = meta.height || null;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const mime_type = inferMimeFromExt(ext);
|
||||
const id = sha256(relFile);
|
||||
|
||||
// GEOLOCATION
|
||||
const location = gps ? await loc(gps.lng, gps.lat) : null;
|
||||
|
||||
results.push({
|
||||
id,
|
||||
name: dirent.name,
|
||||
path: relFile,
|
||||
thub1: relThumbMin,
|
||||
thub2: relThumbAvg,
|
||||
gps,
|
||||
data: timeRaw,
|
||||
taken_at: takenAtIso,
|
||||
mime_type,
|
||||
width,
|
||||
height,
|
||||
size_bytes,
|
||||
duration: isVideo ? duration : null,
|
||||
location
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// MAIN
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
async function scanPhoto(dir) {
|
||||
try {
|
||||
const absDir = path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir);
|
||||
const photos = await scanDir(absDir);
|
||||
|
||||
if (SEND_PHOTOS && BASE_URL) {
|
||||
for (const p of photos) {
|
||||
try {
|
||||
await postWithAuth(`${BASE_URL}/photos`, p);
|
||||
} catch (err) {
|
||||
console.error('Errore invio:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (WRITE_INDEX) {
|
||||
const absIndexPath = path.join(WEB_ROOT, INDEX_PATH);
|
||||
await fsp.mkdir(path.dirname(absIndexPath), { recursive: true });
|
||||
await fsp.writeFile(absIndexPath, JSON.stringify(photos, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
return photos;
|
||||
|
||||
} catch (e) {
|
||||
console.error('Errore generale scanPhoto:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = scanPhoto;
|
||||
273
api_v1/scanphoto.js.ok
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
/* eslint-disable no-console */
|
||||
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://api.tuoserver.tld (backend con /auth/login e /photos)
|
||||
const EMAIL = process.env.EMAIL;
|
||||
const PASSWORD = process.env.PASSWORD;
|
||||
|
||||
// Opzioni
|
||||
const SEND_PHOTOS = (process.env.SEND_PHOTOS || 'true').toLowerCase() === 'true'; // invia ogni record via POST /photos
|
||||
const WRITE_INDEX = (process.env.WRITE_INDEX || 'true').toLowerCase() === 'true'; // scrivi public/photos/index.json
|
||||
const WEB_ROOT = process.env.WEB_ROOT || 'public'; // radice dei file serviti
|
||||
const INDEX_PATH = process.env.INDEX_PATH || path.posix.join('photos', 'index.json');
|
||||
|
||||
// estensioni supportate
|
||||
const SUPPORTED_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif']);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// UTILS
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// usa sempre POSIX per i path web (slash '/')
|
||||
function toPosix(p) {
|
||||
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();
|
||||
}
|
||||
|
||||
// normalizza GPS da {Latitude,Longitude,Altitude} -> {lat,lng,alt}
|
||||
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;
|
||||
const toNum = (v) => (typeof v === 'number' ? v : Number(v));
|
||||
const obj = { lat: toNum(lat), lng: toNum(lng) };
|
||||
if (alt != null) obj.alt = toNum(alt);
|
||||
return obj;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// AUTH / POST
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let cachedToken = null;
|
||||
|
||||
async function getToken(force = false) {
|
||||
if (!SEND_PHOTOS) return null;
|
||||
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) {
|
||||
if (!SEND_PHOTOS) return;
|
||||
let token = await getToken();
|
||||
if (!token) throw new Error('Token assente');
|
||||
|
||||
try {
|
||||
await axios.post(url, payload, {
|
||||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
timeout: 20000,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
token = await getToken(true);
|
||||
if (!token) throw err;
|
||||
await axios.post(url, payload, {
|
||||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
timeout: 20000,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// THUMBNAILS
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
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)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
async function scanDir(dirAbs, results = []) {
|
||||
const dirEntries = await fsp.readdir(dirAbs, { withFileTypes: true });
|
||||
|
||||
for (const dirent of dirEntries) {
|
||||
const absPath = path.join(dirAbs, dirent.name);
|
||||
|
||||
if (dirent.isDirectory()) {
|
||||
await scanDir(absPath, results);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ext = path.extname(dirent.name).toLowerCase();
|
||||
if (!SUPPORTED_EXTS.has(ext)) continue;
|
||||
|
||||
console.log('Trovato:', absPath);
|
||||
|
||||
// path relativo (web-safe) rispetto a WEB_ROOT
|
||||
const relFile = toPosix(path.relative(WEB_ROOT, absPath)); // es. photos/original/.../IMG_0092.JPG
|
||||
const relDir = toPosix(path.posix.dirname(relFile)); // es. photos/original/... (POSIX)
|
||||
|
||||
// cartella thumbs parallela
|
||||
const relThumbDir = relDir.replace(/original/i, 'thumbs');
|
||||
const absThumbDir = path.join(WEB_ROOT, relThumbDir);
|
||||
await fsp.mkdir(absThumbDir, { recursive: true });
|
||||
|
||||
const baseName = path.parse(dirent.name).name;
|
||||
const extName = path.parse(dirent.name).ext;
|
||||
|
||||
// path ASSOLUTI (filesystem) per i file thumb
|
||||
const absThumbMin = path.join(absThumbDir, `${baseName}_min${extName}`);
|
||||
const absThumbAvg = path.join(absThumbDir, `${baseName}_avg${extName}`);
|
||||
|
||||
// crea thumbnails
|
||||
await createThumbnails(absPath, absThumbMin, absThumbAvg);
|
||||
|
||||
// path RELATIVI (web) dei thumb
|
||||
const relThumbMin = toPosix(path.relative(WEB_ROOT, absThumbMin)); // photos/thumbs/..._min.JPG
|
||||
const relThumbAvg = toPosix(path.relative(WEB_ROOT, absThumbAvg)); // photos/thumbs/..._avg.JPG
|
||||
|
||||
// EXIF e metadata immagine
|
||||
let tags = {};
|
||||
try {
|
||||
tags = await ExifReader.load(absPath, { expanded: true });
|
||||
} catch (err) {
|
||||
// nessun EXIF: ok
|
||||
}
|
||||
const timeRaw = tags?.exif?.DateTimeOriginal?.value?.[0] || null;
|
||||
const takenAtIso = parseExifDateUtc(timeRaw);
|
||||
const gps = mapGps(tags?.gps);
|
||||
|
||||
// dimensioni e peso
|
||||
let width = null, height = null, size_bytes = null;
|
||||
try {
|
||||
const meta = await sharp(absPath).metadata();
|
||||
width = meta.width || null;
|
||||
height = meta.height || null;
|
||||
const st = await fsp.stat(absPath);
|
||||
size_bytes = st.size;
|
||||
} catch (err) {
|
||||
try {
|
||||
const st = await fsp.stat(absPath);
|
||||
size_bytes = st.size;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const mime_type = inferMimeFromExt(ext);
|
||||
const id = sha256(relFile); // id stabile
|
||||
|
||||
// RECORD allineato all’app
|
||||
results.push({
|
||||
id, // sha256 del path relativo
|
||||
name: dirent.name,
|
||||
path: relFile,
|
||||
thub1: relThumbMin,
|
||||
thub2: relThumbAvg,
|
||||
gps, // {lat,lng,alt} oppure null
|
||||
data: timeRaw, // EXIF originale (legacy)
|
||||
taken_at: takenAtIso, // ISO-8601 UTC
|
||||
mime_type,
|
||||
width,
|
||||
height,
|
||||
size_bytes,
|
||||
location: null,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// MAIN
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
async function scanPhoto(dir) {
|
||||
try {
|
||||
console.log('Inizio scansione:', dir);
|
||||
|
||||
// dir può essere "public/photos/original" o simile
|
||||
const absDir = path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir);
|
||||
const photos = await scanDir(absDir);
|
||||
|
||||
console.log('Trovate', photos.length, 'foto');
|
||||
|
||||
// 1) (opzionale) invio a backend /photos
|
||||
if (SEND_PHOTOS && BASE_URL) {
|
||||
for (const p of photos) {
|
||||
try {
|
||||
await postWithAuth(`${BASE_URL}/photos`, p);
|
||||
} catch (err) {
|
||||
console.error('Errore invio foto:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) (opzionale) scrivo indice statico public/photos/index.json
|
||||
if (WRITE_INDEX) {
|
||||
const absIndexPath = path.join(WEB_ROOT, INDEX_PATH); // es. public/photos/index.json
|
||||
await fsp.mkdir(path.dirname(absIndexPath), { recursive: true });
|
||||
await fsp.writeFile(absIndexPath, JSON.stringify(photos, null, 2), 'utf8');
|
||||
console.log('Scritto indice:', absIndexPath);
|
||||
}
|
||||
|
||||
console.log('Scansione completata');
|
||||
return photos;
|
||||
} catch (e) {
|
||||
console.error('Errore generale scanPhoto:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = scanPhoto;
|
||||
|
||||
// Esempio di esecuzione diretta:
|
||||
// node -e "require('./api_v1/scanphoto')('public/photos/original')"
|
||||
400
api_v1/scanphoto.js.old
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
/* eslint-disable no-console */
|
||||
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 { exec } = require('child_process');
|
||||
|
||||
const BASE_URL = process.env.BASE_URL;
|
||||
const EMAIL = process.env.EMAIL;
|
||||
const PASSWORD = process.env.PASSWORD;
|
||||
|
||||
const SEND_PHOTOS = (process.env.SEND_PHOTOS || 'true').toLowerCase() === 'true';
|
||||
const WRITE_INDEX = (process.env.WRITE_INDEX || 'true').toLowerCase() === 'true';
|
||||
const WEB_ROOT = process.env.WEB_ROOT || 'public';
|
||||
const INDEX_PATH = process.env.INDEX_PATH || path.posix.join('photos', 'index.json');
|
||||
|
||||
// Estensioni supportate (immagini + video)
|
||||
const SUPPORTED_EXTS = new Set([
|
||||
'.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif',
|
||||
'.mp4', '.mov', '.m4v'
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// UTILS
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function toPosix(p) {
|
||||
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';
|
||||
|
||||
case '.mp4': return 'video/mp4';
|
||||
case '.mov': return 'video/quicktime';
|
||||
case '.m4v': return 'video/x-m4v';
|
||||
|
||||
default: return 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// GPS — FOTO (ExifReader)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function extractGpsFromExif(tags) {
|
||||
if (!tags?.gps) return null;
|
||||
|
||||
const lat = tags.gps.Latitude;
|
||||
const lng = tags.gps.Longitude;
|
||||
const alt = tags.gps.Altitude;
|
||||
|
||||
if (lat == null || lng == null) return null;
|
||||
|
||||
return {
|
||||
lat: Number(lat),
|
||||
lng: Number(lng),
|
||||
alt: alt != null ? Number(alt) : null
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// GPS — VIDEO (exiftool) — FUNZIONA SEMPRE
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function extractGpsWithExiftool(videoPath) {
|
||||
return new Promise((resolve) => {
|
||||
const cmd = `exiftool -n -G1 -a -gps:all -quicktime:all -user:all "${videoPath}"`;
|
||||
exec(cmd, (err, stdout, stderr) => {
|
||||
|
||||
console.log("=== EXIFTOOL RAW OUTPUT ===");
|
||||
console.log(stdout);
|
||||
console.log("=== FINE RAW OUTPUT ===");
|
||||
|
||||
if (err || !stdout) return resolve(null);
|
||||
|
||||
// 1) UserData: GPS Coordinates : 44.1816 12.1251
|
||||
const userData = stdout.match(/GPS Coordinates\s*:\s*([0-9\.\-]+)\s+([0-9\.\-]+)/i);
|
||||
if (userData) {
|
||||
return resolve({
|
||||
lat: Number(userData[1]),
|
||||
lng: Number(userData[2]),
|
||||
alt: null
|
||||
});
|
||||
}
|
||||
|
||||
// 2) GPSLatitude / GPSLongitude (fallback)
|
||||
const lat1 = stdout.match(/GPSLatitude\s*:\s*([0-9\.\-]+)/i);
|
||||
const lng1 = stdout.match(/GPSLongitude\s*:\s*([0-9\.\-]+)/i);
|
||||
if (lat1 && lng1) {
|
||||
return resolve({
|
||||
lat: Number(lat1[1]),
|
||||
lng: Number(lng1[1]),
|
||||
alt: null
|
||||
});
|
||||
}
|
||||
|
||||
// 3) QuickTime:GPSCoordinates (fallback)
|
||||
const coords = stdout.match(/GPSCoordinates\s*:\s*([0-9\.\-]+)\s+([0-9\.\-]+)/i);
|
||||
if (coords) {
|
||||
return resolve({
|
||||
lat: Number(coords[1]),
|
||||
lng: Number(coords[2]),
|
||||
alt: null
|
||||
});
|
||||
}
|
||||
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// AUTH / POST
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let cachedToken = null;
|
||||
|
||||
async function getToken(force = false) {
|
||||
if (!SEND_PHOTOS) return null;
|
||||
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) {
|
||||
if (!SEND_PHOTOS) return;
|
||||
|
||||
let token = await getToken();
|
||||
if (!token) throw new Error('Token assente');
|
||||
|
||||
try {
|
||||
await axios.post(url, payload, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 20000,
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
token = await getToken(true);
|
||||
if (!token) throw err;
|
||||
|
||||
await axios.post(url, payload, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 20000,
|
||||
});
|
||||
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// VIDEO: ffmpeg thumbnail
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function createVideoThumbnail(videoPath, thumbMinPath, thumbAvgPath) {
|
||||
return new Promise((resolve) => {
|
||||
const cmd = `
|
||||
ffmpeg -y -i "${videoPath}" -ss 00:00:01.000 -vframes 1 "${thumbAvgPath}" &&
|
||||
ffmpeg -y -i "${thumbAvgPath}" -vf "scale=100:-1" "${thumbMinPath}"
|
||||
`;
|
||||
|
||||
exec(cmd, (err) => {
|
||||
if (err) console.error("Errore thumbnail video:", err.message);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// VIDEO: ffprobe metadata
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function probeVideo(videoPath) {
|
||||
return new Promise((resolve) => {
|
||||
const cmd = `ffprobe -v quiet -print_format json -show_format -show_streams "${videoPath}"`;
|
||||
exec(cmd, (err, stdout) => {
|
||||
if (err) {
|
||||
console.error("Errore ffprobe:", err.message);
|
||||
return resolve({});
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(stdout));
|
||||
} catch {
|
||||
resolve({});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// THUMBNAILS IMMAGINI
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
async function scanDir(dirAbs, results = []) {
|
||||
const dirEntries = await fsp.readdir(dirAbs, { withFileTypes: true });
|
||||
|
||||
for (const dirent of dirEntries) {
|
||||
const absPath = path.join(dirAbs, dirent.name);
|
||||
|
||||
if (dirent.isDirectory()) {
|
||||
await scanDir(absPath, results);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ext = path.extname(dirent.name).toLowerCase();
|
||||
if (!SUPPORTED_EXTS.has(ext)) continue;
|
||||
|
||||
const isVideo = ['.mp4', '.mov', '.m4v'].includes(ext);
|
||||
|
||||
console.log('Trovato:', absPath);
|
||||
|
||||
const relFile = toPosix(path.relative(WEB_ROOT, absPath));
|
||||
const relDir = toPosix(path.posix.dirname(relFile));
|
||||
|
||||
const relThumbDir = relDir.replace(/original/i, 'thumbs');
|
||||
const absThumbDir = path.join(WEB_ROOT, relThumbDir);
|
||||
await fsp.mkdir(absThumbDir, { recursive: true });
|
||||
|
||||
const baseName = path.parse(dirent.name).name;
|
||||
|
||||
const absThumbMin = path.join(absThumbDir, `${baseName}_min.jpg`);
|
||||
const absThumbAvg = path.join(absThumbDir, `${baseName}_avg.jpg`);
|
||||
|
||||
if (isVideo) {
|
||||
console.log(">>> È UN VIDEO, ESTRAGGO GPS CON EXIFTOOL:", absPath);
|
||||
await createVideoThumbnail(absPath, absThumbMin, absThumbAvg);
|
||||
} else {
|
||||
await createThumbnails(absPath, absThumbMin, absThumbAvg);
|
||||
}
|
||||
|
||||
const relThumbMin = toPosix(path.relative(WEB_ROOT, absThumbMin));
|
||||
const relThumbAvg = toPosix(path.relative(WEB_ROOT, absThumbAvg));
|
||||
|
||||
let tags = {};
|
||||
try {
|
||||
tags = await ExifReader.load(absPath, { expanded: true });
|
||||
} catch {}
|
||||
|
||||
const timeRaw = tags?.exif?.DateTimeOriginal?.value?.[0] || null;
|
||||
const takenAtIso = parseExifDateUtc(timeRaw);
|
||||
|
||||
let gps = null;
|
||||
|
||||
if (isVideo) {
|
||||
gps = await extractGpsWithExiftool(absPath);
|
||||
} else {
|
||||
gps = extractGpsFromExif(tags);
|
||||
}
|
||||
|
||||
let width = null, height = null, size_bytes = null, duration = null;
|
||||
|
||||
const st = await fsp.stat(absPath);
|
||||
size_bytes = st.size;
|
||||
|
||||
if (isVideo) {
|
||||
const info = await probeVideo(absPath);
|
||||
const stream = info.streams?.find(s => s.width && s.height);
|
||||
if (stream) {
|
||||
width = stream.width;
|
||||
height = stream.height;
|
||||
}
|
||||
duration = info.format?.duration || null;
|
||||
} else {
|
||||
try {
|
||||
const meta = await sharp(absPath).metadata();
|
||||
width = meta.width || null;
|
||||
height = meta.height || null;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const mime_type = inferMimeFromExt(ext);
|
||||
const id = sha256(relFile);
|
||||
|
||||
results.push({
|
||||
id,
|
||||
name: dirent.name,
|
||||
path: relFile,
|
||||
thub1: relThumbMin,
|
||||
thub2: relThumbAvg,
|
||||
gps,
|
||||
data: timeRaw,
|
||||
taken_at: takenAtIso,
|
||||
mime_type,
|
||||
width,
|
||||
height,
|
||||
size_bytes,
|
||||
duration: isVideo ? duration : null,
|
||||
location: null,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// MAIN
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
async function scanPhoto(dir) {
|
||||
try {
|
||||
console.log('Inizio scansione:', dir);
|
||||
|
||||
const absDir = path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir);
|
||||
const photos = await scanDir(absDir);
|
||||
|
||||
console.log('Trovati', photos.length, 'file (foto + video)');
|
||||
|
||||
if (SEND_PHOTOS && BASE_URL) {
|
||||
for (const p of photos) {
|
||||
try {
|
||||
await postWithAuth(`${BASE_URL}/photos`, p);
|
||||
} catch (err) {
|
||||
console.error('Errore invio:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (WRITE_INDEX) {
|
||||
const absIndexPath = path.join(WEB_ROOT, INDEX_PATH);
|
||||
await fsp.mkdir(path.dirname(absIndexPath), { recursive: true });
|
||||
await fsp.writeFile(absIndexPath, JSON.stringify(photos, null, 2), 'utf8');
|
||||
console.log('Scritto indice:', absIndexPath);
|
||||
}
|
||||
|
||||
console.log('Scansione completata');
|
||||
return photos;
|
||||
|
||||
} catch (e) {
|
||||
console.error('Errore generale scanPhoto:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = scanPhoto;
|
||||
144
api_v1/scanphoto.js.orig
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
require('dotenv').config();
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const ExifReader = require('exifreader');
|
||||
const sharp = require('sharp');
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = process.env.BASE_URL;
|
||||
const EMAIL = process.env.EMAIL;
|
||||
const PASSWORD = process.env.PASSWORD;
|
||||
|
||||
// -----------------------------------------------------
|
||||
// LOGIN → ottiene token JWT
|
||||
// -----------------------------------------------------
|
||||
async function getToken() {
|
||||
try {
|
||||
const res = await axios.post(`${BASE_URL}/auth/login`, {
|
||||
email: EMAIL,
|
||||
password: PASSWORD
|
||||
});
|
||||
return res.data.token;
|
||||
} catch (err) {
|
||||
console.error("ERRORE LOGIN:", err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// INVIA FOTO AL SERVER
|
||||
// -----------------------------------------------------
|
||||
async function sendPhoto(json) {
|
||||
const token = await getToken();
|
||||
if (!token) {
|
||||
console.error("Token non ottenuto, impossibile inviare foto");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.post(`${BASE_URL}/photos`, json, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Errore invio foto:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// CREA THUMBNAILS
|
||||
// -----------------------------------------------------
|
||||
async function createThumbnails(filePath, thumbMinPath, thumbAvgPath) {
|
||||
try {
|
||||
await sharp(filePath)
|
||||
.resize(100, 100, { fit: "inside" })
|
||||
.withMetadata()
|
||||
.toFile(thumbMinPath);
|
||||
|
||||
await sharp(filePath)
|
||||
.resize(400)
|
||||
.withMetadata()
|
||||
.toFile(thumbAvgPath);
|
||||
} catch (err) {
|
||||
console.error("Errore creazione thumbnails:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// SCANSIONE RICORSIVA
|
||||
// -----------------------------------------------------
|
||||
async function scanDir(dir, ext, results = []) {
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
await scanDir(filePath, ext, results);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (path.extname(file).toLowerCase() !== ext) continue;
|
||||
|
||||
console.log("Trovato:", file);
|
||||
|
||||
const relDir = dir.replace("public/", "");
|
||||
const thumbDir = path.join("public", relDir.replace("original", "thumbs"));
|
||||
|
||||
if (!fs.existsSync(thumbDir)) {
|
||||
fs.mkdirSync(thumbDir, { recursive: true });
|
||||
}
|
||||
|
||||
const baseName = path.parse(file).name;
|
||||
const extName = path.parse(file).ext;
|
||||
|
||||
const thumbMin = path.join(thumbDir, `${baseName}_min${extName}`);
|
||||
const thumbAvg = path.join(thumbDir, `${baseName}_avg${extName}`);
|
||||
|
||||
await createThumbnails(filePath, thumbMin, thumbAvg);
|
||||
|
||||
// EXIF
|
||||
let tags = {};
|
||||
try {
|
||||
tags = await ExifReader.load(filePath, { expanded: true });
|
||||
} catch {}
|
||||
|
||||
const time = tags?.exif?.DateTimeOriginal?.value?.[0] || null;
|
||||
const gps = tags?.gps || null;
|
||||
|
||||
results.push({
|
||||
name: file,
|
||||
path: relDir + "/" + file,
|
||||
thub1: thumbMin.replace("public/", ""),
|
||||
thub2: thumbAvg.replace("public/", ""),
|
||||
gps,
|
||||
data: time,
|
||||
location: null
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// FUNZIONE PRINCIPALE
|
||||
// -----------------------------------------------------
|
||||
async function scanPhoto(dir) {
|
||||
console.log("Inizio scansione:", dir);
|
||||
|
||||
const photos = await scanDir(dir, ".jpg");
|
||||
|
||||
console.log("Trovate", photos.length, "foto");
|
||||
|
||||
for (const p of photos) {
|
||||
await sendPhoto(p);
|
||||
}
|
||||
|
||||
console.log("Scansione completata");
|
||||
}
|
||||
|
||||
module.exports = scanPhoto;
|
||||
173
api_v1/server ok.js
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
/**
|
||||
* Require necessary libraries
|
||||
*/
|
||||
const fs = require('fs')
|
||||
const bodyParser = require('body-parser')
|
||||
const jsonServer = require('json-server')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const bcrypt = require('bcrypt')
|
||||
//const open = require('open');
|
||||
const path = require('path');
|
||||
const scanPhoto = require('./scanphoto.js')
|
||||
|
||||
|
||||
// JWT confing data
|
||||
const SECRET_KEY = '123456789'
|
||||
const expiresIn = '1h'
|
||||
|
||||
// Create server
|
||||
var server = jsonServer.create()
|
||||
|
||||
// Create router
|
||||
if(fs.existsSync('./api_v1/db.json')){
|
||||
var router = jsonServer.router('./api_v1/db.json')
|
||||
} else {
|
||||
const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8');
|
||||
// to update (sync) current database (db.json) file
|
||||
fs.writeFileSync('api_v1/db.json', initialData);
|
||||
var router = jsonServer.router('./api_v1/db.json')
|
||||
}
|
||||
|
||||
// Create router
|
||||
var router = jsonServer.router('./api_v1/db.json')
|
||||
|
||||
// Users database
|
||||
const userdb = JSON.parse(fs.readFileSync('./api_v1/users.json', 'UTF-8'))
|
||||
|
||||
// Default middlewares
|
||||
server.use(bodyParser.urlencoded({ extended: true }))
|
||||
server.use(bodyParser.json())
|
||||
|
||||
// Create a token from a payload
|
||||
function createToken(payload) {
|
||||
return jwt.sign(payload, SECRET_KEY, { expiresIn })
|
||||
}
|
||||
|
||||
// Verify the token
|
||||
function verifyToken(token) {
|
||||
return jwt.verify(
|
||||
token,
|
||||
SECRET_KEY,
|
||||
(err, decode) => (decode !== undefined ? decode : err)
|
||||
)
|
||||
}
|
||||
|
||||
// Check if the user exists in database
|
||||
function isAuthenticated({ email, password }) {
|
||||
return (
|
||||
userdb.users.findIndex(
|
||||
user =>
|
||||
user.email === email && bcrypt.compareSync(password, user.password)
|
||||
) !== -1
|
||||
)
|
||||
}
|
||||
|
||||
function azz(){
|
||||
const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8');
|
||||
// to update (sync) current database (db.json) file
|
||||
fs.writeFileSync('api_v1/db.json', initialData);
|
||||
router.db.setState(JSON.parse(initialData));
|
||||
console.log('DB resettato');
|
||||
}
|
||||
|
||||
// con 192.168.1.3:7771/ apre http:192.168.1.3:7771/public.index.html
|
||||
server.get('/', (req, res) => {
|
||||
//console.log(req.query)
|
||||
res.sendFile(path.resolve("public/index.html"))
|
||||
})
|
||||
// scansiona le foto
|
||||
server.get('/scan', async (req, res) => {
|
||||
azz();
|
||||
await scanPhoto('./public/photos/original')
|
||||
console.log("Ricaricato")
|
||||
res.send({status: 'Ricaricato'})
|
||||
})
|
||||
|
||||
|
||||
// esempio http:192.168.1.3:7771/files?file=mio.txt
|
||||
server.get('/files', (req, res) => {
|
||||
console.log(req.query)
|
||||
res.sendFile(path.resolve("public/"+req.query.file))
|
||||
})
|
||||
|
||||
server.get('/initDB1',(req, res, next) => {
|
||||
const Data = { photos: []};
|
||||
// to update (sync) current database (db.json) file
|
||||
fs.writeFileSync('api_v1/db.json', JSON.stringify(Data));
|
||||
router.db.setState(Data);
|
||||
res.send({status: 'DB resettato'});
|
||||
//res.sendStatus(200);
|
||||
});
|
||||
|
||||
server.get('/initDB',(req, res, next) => {
|
||||
const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8');
|
||||
// to update (sync) current database (db.json) file
|
||||
fs.writeFileSync('api_v1/db.json', initialData);
|
||||
router.db.setState(JSON.parse(initialData));
|
||||
//router = jsonServer.router('./api_v1/db.json')
|
||||
res.send({status: 'DB resettato'});
|
||||
//res.sendStatus(200);
|
||||
});
|
||||
|
||||
server.get('/log', (req, res) => {
|
||||
console.log(server)
|
||||
})
|
||||
|
||||
server.use((req, res, next) => {
|
||||
//console.log(req.headers);
|
||||
//console.log(req.method);
|
||||
var a = req.path.split("/");
|
||||
if (req.method === 'GET' && a[1] == 'pub' && a.length > 2) {
|
||||
//console.log(req.headers.host);
|
||||
//console.log(a.slice(2).join("/"));
|
||||
res.status(200).sendFile(path.resolve("public/"+a.slice(2).join("/")));
|
||||
//res.sendStatus(200);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Method: POST
|
||||
* Endpoint: /auth/login
|
||||
*/
|
||||
server.post('/auth/login', (req, res) => {
|
||||
const { email, password } = req.body
|
||||
if (isAuthenticated({ email, password }) === false) {
|
||||
const status = 401
|
||||
const message = 'Incorrect email or password'
|
||||
res.status(status).json({ status, message })
|
||||
return
|
||||
}
|
||||
const token = createToken({ email, password })
|
||||
res.status(200).json({ token })
|
||||
})
|
||||
|
||||
/**
|
||||
* Middleware: Check authorization
|
||||
*/
|
||||
server.use(/^(?!\/auth).*$/, (req, res, next) => {
|
||||
if (
|
||||
req.headers.authorization === undefined ||
|
||||
req.headers.authorization.split(' ')[0] !== 'Bearer'
|
||||
) {
|
||||
const status = 401
|
||||
const message = 'Bad authorization header'
|
||||
res.status(status).json({ status, message })
|
||||
return
|
||||
}
|
||||
try {
|
||||
verifyToken(req.headers.authorization.split(' ')[1])
|
||||
next()
|
||||
} catch (err) {
|
||||
const status = 401
|
||||
const message = 'Error: access_token is not valid'
|
||||
res.status(status).json({ status, message })
|
||||
}
|
||||
})
|
||||
|
||||
// Server mount
|
||||
server.use(router)
|
||||
server.listen(3000, () => {
|
||||
console.log('Auth API server runing on port 3000 ...')
|
||||
})
|
||||
152
api_v1/server.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
require('dotenv').config();
|
||||
|
||||
const fs = require('fs');
|
||||
const bodyParser = require('body-parser');
|
||||
const jsonServer = require('json-server');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcrypt');
|
||||
const path = require('path');
|
||||
const scanPhoto = require('./scanphoto.js');
|
||||
|
||||
const SECRET_KEY = process.env.JWT_SECRET || '123456789';
|
||||
const EXPIRES_IN = process.env.JWT_EXPIRES || '1h';
|
||||
const PORT = process.env.SERVER_PORT || 4000;
|
||||
|
||||
const server = jsonServer.create();
|
||||
|
||||
// -----------------------------------------------------
|
||||
// STATIC FILES
|
||||
// -----------------------------------------------------
|
||||
server.use(jsonServer.defaults({
|
||||
static: path.join(__dirname, '../public')
|
||||
}));
|
||||
|
||||
// -----------------------------------------------------
|
||||
// CONFIG ENDPOINT (PUBBLICO)
|
||||
// -----------------------------------------------------
|
||||
server.get('/config', (req, res) => {
|
||||
res.json({
|
||||
baseUrl: process.env.BASE_URL
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// ROUTER DB
|
||||
// -----------------------------------------------------
|
||||
let router;
|
||||
if (fs.existsSync('./api_v1/db.json')) {
|
||||
router = jsonServer.router('./api_v1/db.json');
|
||||
} else {
|
||||
const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8');
|
||||
fs.writeFileSync('api_v1/db.json', initialData);
|
||||
router = jsonServer.router('./api_v1/db.json');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// USERS DB
|
||||
// -----------------------------------------------------
|
||||
const userdb = JSON.parse(fs.readFileSync('./api_v1/users.json', 'UTF-8'));
|
||||
|
||||
server.use(bodyParser.urlencoded({ extended: true }));
|
||||
server.use(bodyParser.json());
|
||||
|
||||
// -----------------------------------------------------
|
||||
// JWT HELPERS
|
||||
// -----------------------------------------------------
|
||||
function createToken(payload) {
|
||||
return jwt.sign(payload, SECRET_KEY, { expiresIn: EXPIRES_IN });
|
||||
}
|
||||
|
||||
function verifyToken(token) {
|
||||
return jwt.verify(token, SECRET_KEY, (err, decode) => decode || err);
|
||||
}
|
||||
|
||||
function isAuthenticated({ email, password }) {
|
||||
return userdb.users.findIndex(
|
||||
user => user.email === email && bcrypt.compareSync(password, user.password)
|
||||
) !== -1;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// RESET DB
|
||||
// -----------------------------------------------------
|
||||
function resetDB() {
|
||||
const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8');
|
||||
fs.writeFileSync('api_v1/db.json', initialData);
|
||||
router.db.setState(JSON.parse(initialData));
|
||||
console.log('DB resettato');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// HOME
|
||||
// -----------------------------------------------------
|
||||
server.get('/', (req, res) => {
|
||||
res.sendFile(path.resolve("public/index.html"));
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// SCAN FOTO
|
||||
// -----------------------------------------------------
|
||||
server.get('/scan', async (req, res) => {
|
||||
resetDB();
|
||||
await scanPhoto('./public/photos/original');
|
||||
console.log("Ricaricato");
|
||||
res.send({ status: 'Ricaricato' });
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// FILE STATICI
|
||||
// -----------------------------------------------------
|
||||
server.get('/files', (req, res) => {
|
||||
res.sendFile(path.resolve("public/" + req.query.file));
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// RESET DB MANUALE
|
||||
// -----------------------------------------------------
|
||||
server.get('/initDB', (req, res) => {
|
||||
resetDB();
|
||||
res.send({ status: 'DB resettato' });
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// LOGIN (PUBBLICO)
|
||||
// -----------------------------------------------------
|
||||
server.post('/auth/login', (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!isAuthenticated({ email, password })) {
|
||||
return res.status(401).json({ status: 401, message: 'Incorrect email or password' });
|
||||
}
|
||||
|
||||
const token = createToken({ email });
|
||||
res.status(200).json({ token });
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// JWT MIDDLEWARE (TUTTO IL RESTO È PROTETTO)
|
||||
// -----------------------------------------------------
|
||||
server.use(/^(?!\/auth).*$/, (req, res, next) => {
|
||||
if (!req.headers.authorization || req.headers.authorization.split(' ')[0] !== 'Bearer') {
|
||||
return res.status(401).json({ status: 401, message: 'Bad authorization header' });
|
||||
}
|
||||
|
||||
try {
|
||||
verifyToken(req.headers.authorization.split(' ')[1]);
|
||||
next();
|
||||
} catch (err) {
|
||||
res.status(401).json({ status: 401, message: 'Error: access_token is not valid' });
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// ROUTER JSON-SERVER
|
||||
// -----------------------------------------------------
|
||||
server.use(router);
|
||||
|
||||
// -----------------------------------------------------
|
||||
// START SERVER
|
||||
// -----------------------------------------------------
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Auth API server running on port ${PORT} ...`);
|
||||
});
|
||||
152
api_v1/server.js.old
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
require('dotenv').config();
|
||||
|
||||
const fs = require('fs');
|
||||
const bodyParser = require('body-parser');
|
||||
const jsonServer = require('json-server');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcrypt');
|
||||
const path = require('path');
|
||||
const scanPhoto = require('./scanphoto.js');
|
||||
|
||||
const SECRET_KEY = process.env.JWT_SECRET || '123456789';
|
||||
const EXPIRES_IN = process.env.JWT_EXPIRES || '1h';
|
||||
const PORT = process.env.SERVER_PORT || 4000;
|
||||
|
||||
const server = jsonServer.create();
|
||||
|
||||
// -----------------------------------------------------
|
||||
// STATIC FILES
|
||||
// -----------------------------------------------------
|
||||
server.use(jsonServer.defaults({
|
||||
static: path.join(__dirname, '../public')
|
||||
}));
|
||||
|
||||
// -----------------------------------------------------
|
||||
// CONFIG ENDPOINT (PUBBLICO)
|
||||
// -----------------------------------------------------
|
||||
server.get('/config', (req, res) => {
|
||||
res.json({
|
||||
baseUrl: process.env.BASE_URL
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// ROUTER DB
|
||||
// -----------------------------------------------------
|
||||
let router;
|
||||
if (fs.existsSync('./api_v1/db.json')) {
|
||||
router = jsonServer.router('./api_v1/db.json');
|
||||
} else {
|
||||
const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8');
|
||||
fs.writeFileSync('api_v1/db.json', initialData);
|
||||
router = jsonServer.router('./api_v1/db.json');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// USERS DB
|
||||
// -----------------------------------------------------
|
||||
const userdb = JSON.parse(fs.readFileSync('./api_v1/users.json', 'UTF-8'));
|
||||
|
||||
server.use(bodyParser.urlencoded({ extended: true }));
|
||||
server.use(bodyParser.json());
|
||||
|
||||
// -----------------------------------------------------
|
||||
// JWT HELPERS
|
||||
// -----------------------------------------------------
|
||||
function createToken(payload) {
|
||||
return jwt.sign(payload, SECRET_KEY, { expiresIn: EXPIRES_IN });
|
||||
}
|
||||
|
||||
function verifyToken(token) {
|
||||
return jwt.verify(token, SECRET_KEY, (err, decode) => decode || err);
|
||||
}
|
||||
|
||||
function isAuthenticated({ email, password }) {
|
||||
return userdb.users.findIndex(
|
||||
user => user.email === email && bcrypt.compareSync(password, user.password)
|
||||
) !== -1;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// RESET DB
|
||||
// -----------------------------------------------------
|
||||
function resetDB() {
|
||||
const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8');
|
||||
fs.writeFileSync('api_v1/db.json', initialData);
|
||||
router.db.setState(JSON.parse(initialData));
|
||||
console.log('DB resettato');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// HOME
|
||||
// -----------------------------------------------------
|
||||
server.get('/', (req, res) => {
|
||||
res.sendFile(path.resolve("public/index.html"));
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// SCAN FOTO
|
||||
// -----------------------------------------------------
|
||||
server.get('/scan', async (req, res) => {
|
||||
resetDB();
|
||||
await scanPhoto('./public/photos/original');
|
||||
console.log("Ricaricato");
|
||||
res.send({ status: 'Ricaricato' });
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// FILE STATICI
|
||||
// -----------------------------------------------------
|
||||
server.get('/files', (req, res) => {
|
||||
res.sendFile(path.resolve("public/" + req.query.file));
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// RESET DB MANUALE
|
||||
// -----------------------------------------------------
|
||||
server.get('/initDB', (req, res) => {
|
||||
resetDB();
|
||||
res.send({ status: 'DB resettato' });
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// LOGIN (PUBBLICO)
|
||||
// -----------------------------------------------------
|
||||
server.post('/auth/login', (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!isAuthenticated({ email, password })) {
|
||||
return res.status(401).json({ status: 401, message: 'Incorrect email or password' });
|
||||
}
|
||||
|
||||
const token = createToken({ email });
|
||||
res.status(200).json({ token });
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// JWT MIDDLEWARE (TUTTO IL RESTO È PROTETTO)
|
||||
// -----------------------------------------------------
|
||||
server.use(/^(?!\/auth).*$/, (req, res, next) => {
|
||||
if (!req.headers.authorization || req.headers.authorization.split(' ')[0] !== 'Bearer') {
|
||||
return res.status(401).json({ status: 401, message: 'Bad authorization header' });
|
||||
}
|
||||
|
||||
try {
|
||||
verifyToken(req.headers.authorization.split(' ')[1]);
|
||||
next();
|
||||
} catch (err) {
|
||||
res.status(401).json({ status: 401, message: 'Error: access_token is not valid' });
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// ROUTER JSON-SERVER
|
||||
// -----------------------------------------------------
|
||||
server.use(router);
|
||||
|
||||
// -----------------------------------------------------
|
||||
// START SERVER
|
||||
// -----------------------------------------------------
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Auth API server running on port ${PORT} ...`);
|
||||
});
|
||||
11
api_v1/super install.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
sudo docker run -it -d -p 7771:3000 -v /home/nvme/plexmediafiles/server/svr:/jwt-json-server/api_v1 -v /home/nvme/plexmediafiles/server/public:/jwt-json-server/public --name json-server-auth node:latest
|
||||
sudo docker exec -it json-server-auth /bin/bash
|
||||
apt update
|
||||
apt upgrade -y
|
||||
apt install nano
|
||||
git clone https://git.patachina.duckdns.org/Fabio/json-server-auth.git jwt-json-server1
|
||||
cp -R jwt-json-server1/* jwt-json-server
|
||||
npm i exifreader
|
||||
npm i path
|
||||
npm i sharp
|
||||
npm i axios
|
||||
51
api_v1/tools.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Required libraries
|
||||
*/
|
||||
const bcrypt = require('bcrypt')
|
||||
const readLine = require('readline')
|
||||
const async = require('async')
|
||||
|
||||
// Password hash method
|
||||
const hashPassword = plain => bcrypt.hashSync(plain, 8)
|
||||
|
||||
// Ask user password method
|
||||
function askPassword(question) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const rl = readLine.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
|
||||
rl.question(question, answer => {
|
||||
rl.close()
|
||||
resolve(answer)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Generate hash password method
|
||||
async function generateHash() {
|
||||
try {
|
||||
console.log('**********************************')
|
||||
console.log('** Password hash script **')
|
||||
console.log('**********************************')
|
||||
|
||||
const passwordAnswer = await askPassword(
|
||||
'Please give me a password to hash: '
|
||||
)
|
||||
|
||||
if (passwordAnswer != '') {
|
||||
const hashedPassword = hashPassword(passwordAnswer)
|
||||
const compare = bcrypt.compareSync(passwordAnswer, hashedPassword)
|
||||
await console.log('Hashed password:', hashedPassword)
|
||||
await console.log('Valdiation:', compare)
|
||||
} else {
|
||||
console.log('You need write something. Script aborted!')
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
generateHash()
|
||||
28
api_v1/users.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"users": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Freddie",
|
||||
"email": "freddie@queen.com",
|
||||
"password": "$2b$08$BjbCY62KrjAvvqs/cURqFu5F4vgcsAwgHDxSAn/kX5lHjLFPQLrn."
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Brian",
|
||||
"email": "brian@queen.com",
|
||||
"password": "$2b$08$BjbCY62KrjAvvqs/cURqFu5F4vgcsAwgHDxSAn/kX5lHjLFPQLrn."
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Rogery",
|
||||
"email": "roger@queen.com",
|
||||
"password": "$2b$08$BjbCY62KrjAvvqs/cURqFu5F4vgcsAwgHDxSAn/kX5lHjLFPQLrn."
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Fabio",
|
||||
"email": "fabio@gmail.com",
|
||||
"password": "$2b$08$g0UWN6RnN7e.8rX3fuXSSOSJDTvucu./0FAU.yXp0wx4SJXyeaU3."
|
||||
}
|
||||
]
|
||||
}
|
||||
0
db.json
Normal file
143
index.html
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Page Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>This is a Heading</h1>
|
||||
<p>This is a paragraph.</p>
|
||||
<button type="button" onclick="scan()">Scan</button>
|
||||
<button type="button" onclick="read()">Read</button>
|
||||
<button type="button" onclick="writ()">write</button>
|
||||
<button type="button" onclick="azz()">Azz</button>
|
||||
<button type="button" onclick="log()">Log DB</button>
|
||||
<button type="button" onclick="myReset()">Reset</button>
|
||||
<script>
|
||||
|
||||
var tok;
|
||||
var db;
|
||||
function read() {
|
||||
myGet();
|
||||
}
|
||||
|
||||
function writ() {
|
||||
myPost({'email':'fabio@gmail.com', 'password':'master66'})
|
||||
}
|
||||
|
||||
function scan() {
|
||||
fetch('http://192.168.1.3:7771/scan', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function log() {
|
||||
lineGet("log")
|
||||
console.log("log inviato");
|
||||
}
|
||||
|
||||
function myReset() {
|
||||
lineGet("initDB")
|
||||
console.log("reset inviato");
|
||||
}
|
||||
|
||||
async function azz(){
|
||||
await azz1();
|
||||
console.log("azzerato");
|
||||
}
|
||||
|
||||
async function azz1() {
|
||||
for(let v =0; v < db.length; v++){
|
||||
//console.log(db[v].id);
|
||||
myDel(db[v].id);
|
||||
if (v==db.length-1){
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toc() {
|
||||
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 => {
|
||||
tok = user1.token;
|
||||
console.log(tok);
|
||||
})
|
||||
}
|
||||
|
||||
function myPost(json) {
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append('Authorization', 'Bearer ' + tok);
|
||||
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(user));
|
||||
}
|
||||
|
||||
function myGet() {
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append('Authorization', 'Bearer ' + tok);
|
||||
fetch('http://192.168.1.3:7771/photos', {
|
||||
method: 'GET',
|
||||
headers: myHeaders,
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(user => {
|
||||
console.log(user);
|
||||
db=user;
|
||||
});
|
||||
}
|
||||
|
||||
function myDel(id) {
|
||||
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append('Authorization', 'Bearer ' + tok);
|
||||
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/'+id, {
|
||||
method: 'DELETE',
|
||||
headers: myHeaders,
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(user => console.log(""));
|
||||
|
||||
}
|
||||
|
||||
function lineGet(dir) {
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append('Authorization', 'Bearer ' + tok);
|
||||
fetch('http://192.168.1.3:7771/'+dir, {
|
||||
method: 'GET',
|
||||
headers: myHeaders,
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(user => {
|
||||
console.log(user);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
toc();
|
||||
myGet();
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
21
license.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 - Igor Antun
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
2714
package-lock.json
generated
Normal file
39
package.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "jwt-json-server",
|
||||
"version": "0.0.1",
|
||||
"description": "Building a Fake and JWT Protected REST API with json-server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start-no-auth": "json-server --watch ./api_v1/db.json --host 0.0.0.0 -p 4000 -s ./public",
|
||||
"start": "node ./api_v1/server.js --host 0.0.0.0 -p 4000 -s ./public",
|
||||
"mock-data": "node ./api_v1/generateData.js > ./api_v1/db.json",
|
||||
"hash": "node ./api_v1/tools.js",
|
||||
"search": "node ./api_v1/search.js",
|
||||
"photo": "node ./api_v1/photo.js",
|
||||
"photo1": "node ./api_v1/photo1.js",
|
||||
"geo": "node ./api_v1/georev.js",
|
||||
"geo1": "node ./api_v1/georev1.js",
|
||||
"geo2": "node ./api_v1/georev2.js",
|
||||
"geo3": "node ./api_v1/georev3.js"
|
||||
},
|
||||
"keywords": [
|
||||
"api"
|
||||
],
|
||||
"author": "Fabio",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tensorflow/tfjs-core": "^4.21.0",
|
||||
"async": "^3.2.6",
|
||||
"axios": "^1.7.7",
|
||||
"bcrypt": "^5.1.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"exifreader": "^4.23.6",
|
||||
"face-api.js": "^0.20.0",
|
||||
"faker": "^5.5.3",
|
||||
"json-server": "^0.17.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"path": "^0.12.7",
|
||||
"ramda": "^0.30.1",
|
||||
"sharp": "^0.33.5"
|
||||
}
|
||||
}
|
||||
88
public/index.html
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Photo Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="login-box" style="padding:20px;">
|
||||
<h2>Login</h2>
|
||||
<input id="email" type="text" placeholder="Email"><br><br>
|
||||
<input id="password" type="password" placeholder="Password"><br><br>
|
||||
<button onclick="login()">Accedi</button>
|
||||
</div>
|
||||
|
||||
<div id="app" style="display:none;">
|
||||
<h2>Gestione Foto</h2>
|
||||
|
||||
<button onclick="scan()">Scansiona Foto</button>
|
||||
<button onclick="resetDB()">Reset DB</button>
|
||||
|
||||
<pre id="out"></pre>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let BASE_URL = null;
|
||||
let token = null;
|
||||
|
||||
async function loadConfig() {
|
||||
const res = await fetch('/config');
|
||||
const cfg = await res.json();
|
||||
BASE_URL = cfg.baseUrl;
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const email = document.getElementById("email").value;
|
||||
const password = document.getElementById("password").value;
|
||||
|
||||
const res = await fetch(`${BASE_URL}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
alert("Credenziali errate");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
token = data.token;
|
||||
|
||||
document.getElementById("login-box").style.display = "none";
|
||||
document.getElementById("app").style.display = "block";
|
||||
|
||||
await readDB();
|
||||
}
|
||||
|
||||
async function readDB() {
|
||||
const res = await fetch(`${BASE_URL}/photos`, {
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
});
|
||||
|
||||
const db = await res.json();
|
||||
document.getElementById("out").textContent = JSON.stringify(db, null, 2);
|
||||
}
|
||||
|
||||
async function scan() {
|
||||
await fetch(`${BASE_URL}/scan`, {
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
});
|
||||
await readDB();
|
||||
}
|
||||
|
||||
async function resetDB() {
|
||||
await fetch(`${BASE_URL}/initDB`, {
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
});
|
||||
await readDB();
|
||||
}
|
||||
|
||||
window.onload = async () => {
|
||||
await loadConfig();
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
82
public/index.html.old
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Gestione Foto</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Gestione Foto</h1>
|
||||
|
||||
<button onclick="scan()">Scansiona</button>
|
||||
<button onclick="readDB()">Leggi DB</button>
|
||||
<button onclick="resetDB()">Reset DB</button>
|
||||
<button onclick="deleteAll()">Cancella tutto</button>
|
||||
|
||||
<pre id="out"></pre>
|
||||
|
||||
<script>
|
||||
let BASE_URL = null;
|
||||
let tok = null;
|
||||
let db = [];
|
||||
|
||||
async function loadConfig() {
|
||||
const res = await fetch('/config');
|
||||
const cfg = await res.json();
|
||||
BASE_URL = cfg.baseUrl;
|
||||
}
|
||||
|
||||
async function start() {
|
||||
await loadConfig();
|
||||
await login();
|
||||
await readDB();
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const res = await fetch(`${BASE_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'fabio@gmail.com', password: 'master66' })
|
||||
});
|
||||
const data = await res.json();
|
||||
tok = data.token;
|
||||
}
|
||||
|
||||
async function readDB() {
|
||||
const res = await fetch(`${BASE_URL}/photos`, {
|
||||
headers: { 'Authorization': 'Bearer ' + tok }
|
||||
});
|
||||
db = await res.json();
|
||||
document.getElementById('out').textContent = JSON.stringify(db, null, 2);
|
||||
}
|
||||
|
||||
async function scan() {
|
||||
await fetch(`${BASE_URL}/scan`, {
|
||||
headers: { 'Authorization': 'Bearer ' + tok }
|
||||
});
|
||||
await readDB();
|
||||
}
|
||||
|
||||
async function resetDB() {
|
||||
await fetch(`${BASE_URL}/initDB`, {
|
||||
headers: { 'Authorization': 'Bearer ' + tok }
|
||||
});
|
||||
await readDB();
|
||||
}
|
||||
|
||||
async function deleteAll() {
|
||||
for (const item of db) {
|
||||
await fetch(`${BASE_URL}/photos/${item.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + tok }
|
||||
});
|
||||
}
|
||||
await readDB();
|
||||
}
|
||||
|
||||
start();
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
2905
public/photos/index.json
Normal file
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0092.JPG
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0099.JPG
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0100.JPG
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0102.JPG
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0103.JPG
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0104.JPG
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0106.JPG
Normal file
|
After Width: | Height: | Size: 4.1 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0107.JPG
Normal file
|
After Width: | Height: | Size: 3 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0108.JPG
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0109.JPG
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0110.JPG
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0112.JPG
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0113.JPG
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0114.JPG
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0116.JPG
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0119.JPG
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0120.JPG
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0122.JPG
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0123.JPG
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0124.JPG
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0125.JPG
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0126.JPG
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0133.JPG
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0134.JPG
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0135.JPG
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0136.JPG
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0137.JPG
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0138.JPG
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0139.JPG
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0140.JPG
Normal file
|
After Width: | Height: | Size: 4.4 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0141.JPG
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0143.JPG
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0145.JPG
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0146.JPG
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0147.JPG
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0148.JPG
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0149.JPG
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0150.JPG
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0152.JPG
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0153.JPG
Normal file
|
After Width: | Height: | Size: 4.6 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0154.JPG
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0155.JPG
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0156.JPG
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0157.JPG
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0160.JPG
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0162.JPG
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0163.JPG
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0164.JPG
Normal file
|
After Width: | Height: | Size: 3 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0165.JPG
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0166.JPG
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0167.JPG
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0170.JPG
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0171.JPG
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0172.JPG
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0174.JPG
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0175.JPG
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0176.JPG
Normal file
|
After Width: | Height: | Size: 951 KiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0177.JPG
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0178.JPG
Normal file
|
After Width: | Height: | Size: 4.4 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0179.JPG
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0180.JPG
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0182.JPG
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0183.JPG
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0185.JPG
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0188.JPG
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
public/photos/original/2017Irlanda19-29ago/IMG_0190.JPG
Normal file
|
After Width: | Height: | Size: 1.8 MiB |