605 lines
11 KiB
JavaScript
605 lines
11 KiB
JavaScript
// 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;
|