server_photo_s85js/routes/autoScan.js
2026-08-11 08:45:04 +02:00

605 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// routes/autoScan.js
const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs');
const fsp = require('fs/promises');
const db = require('../db/knex');
const { WEB_ROOT } = require('../api_v1/config');
const scanFile = require('../api_v1/scanner/scanFileEntry');
const scanPhotoSingle = require('../api_v1/scanner/scanPhotoSingle');
const scanCartella = require('../api_v1/scanner/scanCartella');
const scanActivitySingle = require('../api_v1/scanner/scanActivitySingle');
const scanActivityCartella = require('../api_v1/scanner/scanActivityCartella');
const createCleanupFunctions = require('../api_v1/scanner/orphanCleanup');
const { deleteThumbsById } = createCleanupFunctions(db, parseInt(process.env.PHOTO_RETENTION_DAYS || "30", 10));
const createDeleteFolderFunctions = require('../api_v1/scanner/deleteFolder');
const { deleteFolderForUser } = createDeleteFolderFunctions(db, parseInt(process.env.PHOTO_RETENTION_DAYS || "30", 10));
const wss = require('../ws-server');
// ===============================
// Helpers
// ===============================
function relPath2Cartella(p) {
const parts = p.split("/").filter(Boolean);
return parts.slice(3).join("/");
}
function getArea(relPath) {
const parts = relPath.split('/').filter(Boolean);
return parts[0];
}
const { sha256 } = require('../api_v1/scanner/utils');
function activityId(user, cartella, file) {
return sha256(`${user}/${cartella}/${file}`);
}
// ===============================
// AUTO SCAN ROUTER
// ===============================
router.post('/', async (req, res) => {
const { type, file, path: relPath, user } = req.body;
const area = getArea(relPath);
const cart = relPath2Cartella(relPath);
const absFile = path.join(
__dirname,
'..',
WEB_ROOT,
relPath,
file
);
try {
switch (type) {
// ===============================
// ADD_DIR — aggiunta cartella (bulk)
// ===============================
case 'ADD_DIR': {
// ===================================
// ACTIVITIES
// ===================================
if (area === 'activities') {
const absCartella = path.join(
__dirname,
'..',
WEB_ROOT,
'activities',
user,
'original',
file
);
const newActivities = [];
const CURRENT = { value: 0 };
const TOTAL_FILES = 0;
const start = Date.now();
const since = new Date().toISOString();
await scanActivityCartella(
db,
user,
file,
absCartella,
newActivities,
async () => {},
CURRENT,
TOTAL_FILES,
start,
deleteThumbsById,
async () => {},
null
);
wss.broadcastToUser(user, {
type: "add_dir",
mode: "activities",
bulk: true,
count: newActivities.length,
since
});
wss.broadcastToUser(user, {
type: "add_dir_done",
mode: "activities",
folder: file,
count: newActivities.length
});
return res.json({
status: 'OK',
action: 'ADD_DIR',
entity: 'activities',
count: newActivities.length
});
}
// ===================================
// PHOTOS
// ===================================
const absCartella = path.join(
__dirname,
'..',
WEB_ROOT,
"photos",
user,
"original",
file
);
const BULK_THRESHOLD =
parseInt(process.env.WS_BULK_THRESHOLD || "500", 10);
let count = 0;
let bulkMode = false;
const since = new Date().toISOString();
for await (const f of scanCartella(user, file, absCartella, db)) {
count++;
await db('deleted_hard')
.where({ id: f.id, user })
.del();
await db('photos')
.where({ id: f.id, user })
.del();
const arr = [];
await scanPhotoSingle(
db,
user,
file,
f,
arr
);
if (!bulkMode && count > BULK_THRESHOLD) {
bulkMode = true;
wss.broadcastToUser(user, {
type: "add_dir",
folder: file,
mode: "bulk",
count,
since
});
continue;
}
if (!bulkMode) {
wss.broadcastToUser(user, {
type: "added",
id: f.id
});
}
}
if (bulkMode) {
wss.broadcastToUser(user, {
type: "add_dir_done",
folder: file,
mode: "bulk",
count,
since
});
}
return res.json({
status: 'OK',
action: 'ADD_DIR',
count,
bulkMode
});
}
// ===============================
// DEL — singolo file
// ===============================
case 'DEL': {
// ===================================
// ACTIVITIES
// ===================================
/*if (area === 'activities') {
const id = activityId(
user,
cart,
file
);
await db('activities')
.where({ id, user })
.del();
await db('deleted_hard_activities')
.insert({
id,
user,
deleted_at: new Date().toISOString()
})
.onConflict('id')
.merge();
wss.broadcastToUser(user, {
type: "removed",
mode: "activities",
id
});
return res.json({
status: 'OK',
action: 'DEL',
entity: 'activities',
id
});
}*/
if (area === 'activities') {
// 1⃣ Trovo l'attività in base a cartella + file_name
const activity = await db('activities')
.where({
user,
cartella: cart,
file_name: file
})
.first();
if (!activity) {
// Nessuna attività trovata: non c'è nulla da cancellare
return res.json({
status: 'OK',
action: 'DEL',
entity: 'activities',
skipped: true
});
}
const id = activity.id;
// 2⃣ Cancello dal DB principale
await db('activities')
.where({ id, user })
.del();
// 3⃣ Segno la cancellazione hard
await db('deleted_hard_activities')
.insert({
id,
user,
deleted_at: new Date().toISOString()
})
.onConflict('id')
.merge();
// 4⃣ Notifica WS
wss.broadcastToUser(user, {
type: "removed",
mode: "activities",
id
});
return res.json({
status: 'OK',
action: 'DEL',
entity: 'activities',
id
});
}
// ===================================
// PHOTOS
// ===================================
const e = await scanFile(user, cart, absFile);
await deleteThumbsById(e.id);
await db('photos')
.where({ id: e.id, user })
.del();
try {
await db('deleted_hard')
.insert({
id: e.id,
user,
deleted_at: new Date().toISOString()
})
.onConflict('id')
.merge();
} catch (err) {
console.warn(
"⚠️ deleted_hard già presente o errore non bloccante:",
err.message
);
}
wss.broadcastToUser(user, {
type: "del",
id: e.id
});
return res.json({
status: 'OK',
action: 'DEL',
id: e.id
});
}
// ===============================
// ADD — singolo file
// ===============================
case 'ADD': {
// ===================================
// ACTIVITIES
// ===================================
if (area === 'activities') {
const ext = path.extname(file).toLowerCase();
if (!['.fit', '.gpx', '.tcx'].includes(ext)) {
return res.status(400).json({
error: `Estensione attività non supportata: ${ext}`
});
}
let st;
try {
st = await fsp.stat(absFile);
} catch {
return res.status(404).json({
error: 'File attività non trovato'
});
}
const f = {
id: `${cart}/${file}`,
name: file,
file_name: file,
relPath: `${cart}/${file}`, // ✔ percorso corretto
absPath: absFile,
ext,
stat: st,
path: `/activities/${user}/original/${cart}/${file}`
};
const newActivities = [];
const activity = await scanActivitySingle(
db,
user,
cart,
f,
newActivities
);
if (!activity) {
return res.json({
status: 'OK',
action: 'ADD',
skipped: true
});
}
wss.broadcastToUser(user, {
type: 'added',
mode: 'activities',
id: activity.id
});
return res.json({
status: 'OK',
action: 'ADD',
entity: 'activities',
id: activity.id
});
}
// ===================================
// PHOTOS
// ===================================
const f = await scanFile(
user,
cart,
absFile
);
await db('deleted_hard')
.where({ id: f.id, user })
.del();
await db('photos')
.where({ id: f.id, user })
.del();
const newFiles = [];
await scanPhotoSingle(
db,
user,
cart,
f,
newFiles
);
wss.broadcastToUser(user, {
type: "added",
id: f.id
});
return res.json({
status: 'OK',
action: 'ADD',
id: f.id
});
}
// ===============================
// DEL_DIR — rimozione cartella (bulk)
// ===============================
case 'DEL_DIR': {
// ===================================
// ACTIVITIES
// ===================================
if (area === 'activities') {
const activities = await db('activities')
.where({
user,
cartella: file
});
for (const a of activities) {
await db('activities')
.where({ id: a.id, user })
.del();
await db('deleted_hard_activities')
.insert({
id: a.id,
user,
deleted_at: new Date().toISOString()
})
.onConflict('id')
.merge();
wss.broadcastToUser(user, {
type: "removed",
mode: "activities",
id: a.id
});
}
return res.json({
status: "OK",
action: "DEL_DIR",
entity: "activities",
count: activities.length
});
}
// ===================================
// PHOTOS
// ===================================
const photos = await db('photos')
.where({
user,
cartella: file
});
const BULK_THRESHOLD =
parseInt(process.env.WS_BULK_THRESHOLD || "500", 10);
const since = new Date().toISOString();
const bulkMode =
photos.length > BULK_THRESHOLD;
if (bulkMode) {
wss.broadcastToUser(user, {
type: "del_dir",
folder: file,
mode: "bulk",
count: photos.length,
since
});
}
for (const p of photos) {
await db('photos')
.where({ id: p.id, user })
.del();
await deleteThumbsById(p.id);
await db('deleted_hard')
.insert({
id: p.id,
user,
deleted_at: new Date().toISOString()
});
if (!bulkMode) {
wss.broadcastToUser(user, {
type: "del",
id: p.id
});
}
}
if (bulkMode) {
wss.broadcastToUser(user, {
type: "del_dir_done",
folder: file,
mode: "bulk",
count: photos.length,
since
});
}
return res.json({
status: 'OK',
action: 'DEL_DIR',
folder: file,
count: photos.length,
bulkMode
});
}
default:
return res.status(400).json({ error: 'Tipo non valido' });
}
} catch (err) {
console.error('Errore auto_scan:', err);
res.status(500).json({ error: 'Errore durante auto_scan', details: err.message });
}
});
module.exports = router;