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

732 lines
19 KiB
JavaScript

// routes/activities.js
const express = require('express');
const router = express.Router();
const path = require('path');
const fsp = require('fs/promises');
const db = require('../db/knex');
const wss = require('../ws-server');
const normalizeActivity = require('../helpers/normalizeActivity');
// -----------------------------------------------------
// File helpers hard delete attività
// -----------------------------------------------------
const PUBLIC_ROOT = path.resolve(__dirname, '..', 'public');
const ACTIVITIES_ROOT = path.join(PUBLIC_ROOT, 'activities');
const DEL_ACTIVITIES_ROOT = path.join(PUBLIC_ROOT, 'del_activities');
function safeJoin(base, ...parts) {
const resolvedBase = path.resolve(base);
const resolved = path.resolve(base, ...parts);
if (!resolved.startsWith(resolvedBase)) {
throw new Error(`Path non sicuro: ${resolved}`);
}
return resolved;
}
function getOriginalActivityPath(row) {
return safeJoin(
ACTIVITIES_ROOT,
row.user,
'original',
row.cartella || '',
row.file_name
);
}
function getDeletedActivityPath(row) {
return safeJoin(
DEL_ACTIVITIES_ROOT,
row.user,
'original',
row.cartella || '',
row.file_name
);
}
async function pathExists(filePath) {
try {
await fsp.access(filePath);
return true;
} catch {
return false;
}
}
async function moveFileSafe(src, dst) {
if (!src || !dst) return false;
const exists = await pathExists(src);
if (!exists) {
console.warn(`⚠️ File attività non trovato, skip move: ${src}`);
return false;
}
// Crea automaticamente public/del_activities/<user>/original/<cartella>/
await fsp.mkdir(path.dirname(dst), { recursive: true });
try {
await fsp.rename(src, dst);
return true;
} catch (err) {
// fallback se source/destination sono su filesystem diversi
if (err.code === 'EXDEV') {
await fsp.copyFile(src, dst);
await fsp.unlink(src);
return true;
}
throw err;
}
}
// -----------------------------------------------------
// POST /activities
// Salva attività inviata dallo scanner locale
// -----------------------------------------------------
router.post('/', async (req, res) => {
const a = req.body;
const start = Date.now();
console.log(`📥 [POST /activities] Richiesta at=${new Date().toISOString()}`);
console.log("📥 Body:", a);
try {
console.log(`📸 [DB INSERT] Salvo activity id=${a.id} user=${a.user}`);
await db('activities')
.insert({
id: a.id,
user: a.user,
cartella: a.cartella,
file_name: a.file_name,
file_extension: a.file_extension,
format: a.format,
display_name: a.display_name,
route_name: a.route_name,
title: a.title,
description: a.description,
notes: a.notes,
sport: a.sport,
activity_type: a.activity_type,
device_manufacturer: a.device_manufacturer,
device_model: a.device_model,
source_app: a.source_app,
start_time_millis: a.start_time_millis,
end_time_millis: a.end_time_millis,
duration_millis: a.duration_millis,
distance_meters: a.distance_meters,
elevation_gain_meters: a.elevation_gain_meters,
elevation_loss_meters: a.elevation_loss_meters,
min_elevation_meters: a.min_elevation_meters,
max_elevation_meters: a.max_elevation_meters,
file_distance_meters: a.file_distance_meters,
file_elevation_gain_meters: a.file_elevation_gain_meters,
computed_distance_meters: a.computed_distance_meters,
computed_elevation_gain_meters: a.computed_elevation_gain_meters,
average_speed_mps: a.average_speed_mps,
max_speed_mps: a.max_speed_mps,
average_pace_sec_per_km: a.average_pace_sec_per_km,
average_heart_rate: a.average_heart_rate,
max_heart_rate: a.max_heart_rate,
average_cadence: a.average_cadence,
max_cadence: a.max_cadence,
calories: a.calories,
power_avg: a.power_avg,
power_max: a.power_max,
point_count: a.point_count,
segment_count: a.segment_count,
lap_count: a.lap_count,
min_latitude: a.min_latitude,
max_latitude: a.max_latitude,
min_longitude: a.min_longitude,
max_longitude: a.max_longitude,
center_latitude: a.center_latitude,
center_longitude: a.center_longitude,
size_bytes: a.size_bytes,
last_modified_millis: a.last_modified_millis,
date_added_millis: a.date_added_millis,
date_modified_millis: a.date_modified_millis,
last_scan_millis: a.last_scan_millis,
is_favorite: a.is_favorite,
is_hidden: a.is_hidden,
rating: a.rating,
tags: a.tags ? JSON.stringify(a.tags) : null,
track_preview: a.track_preview ? JSON.stringify(a.track_preview) : null,
scan_status: a.scan_status,
scan_error: a.scan_error,
diagnostics: a.diagnostics ? JSON.stringify(a.diagnostics) : null,
location: a.location ? JSON.stringify(a.location) : null,
path: a.path,
fast_hash: a.fast_hash,
deleted_at: a.deleted_at || null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.onConflict('id')
.merge();
wss.broadcastToUser(a.user, {
type: "added",
id: a.id,
entity: "activity"
});
console.log(`✅ [POST /activities] Activity id=${a.id} salvata durata=${Date.now() - start}ms`);
res.json({ ok: true });
} catch (err) {
console.error("❌ Errore POST /activities:", err);
res.status(500).json({ error: err.message });
}
});
// -----------------------------------------------------
// GET /activities/changes
// Progressive sync
// -----------------------------------------------------
router.get('/changes', async (req, res) => {
const user = req.user.name;
const since = req.query.since;
const start = Date.now();
console.log(
`🔄 [SYNC SINCE] GET /activities/changes user=${user} since=${since} at=${new Date().toISOString()}`
);
if (!since) {
console.log("⚠️ [SYNC SINCE] Richiesta senza ?since");
return res.status(400).json({
error: "Missing ?since=timestamp"
});
}
try {
let rows;
if (user === "Admin") {
rows = await db("activities")
.where(builder => {
builder
.where("created_at", ">", since)
.orWhere("updated_at", ">", since);
})
.orderBy("updated_at", "desc");
} else {
rows = await db("activities")
.whereIn("user", [user, "Common"])
.andWhere(builder => {
builder
.where("created_at", ">", since)
.orWhere("updated_at", ">", since);
})
.orderBy("updated_at", "desc");
}
console.log(
`🔄 [SYNC SINCE] Risposta: ${rows.length} attività durata=${Date.now() - start}ms`
);
res.json(rows.map(normalizeActivity));
} catch (err) {
console.error("❌ Errore GET /activities/changes:", err);
res.status(500).json({
error: err.message
});
}
});
// -----------------------------------------------------
// GET /activities/byIds
// -----------------------------------------------------
router.get('/byIds', async (req, res) => {
const ids = req.query.id;
const users = Array.isArray(req.query.user)
? req.query.user
: [req.query.user];
const start = Date.now();
console.log(`📤 [GET /activities/byIds] at=${new Date().toISOString()}`);
console.log("📤 ids:", ids);
console.log("📤 users:", users);
if (!ids) return res.json([]);
const list = Array.isArray(ids) ? ids : [ids];
try {
let rows;
if (users.includes("Admin")) {
rows = await db('activities').whereIn('id', list);
} else {
rows = await db('activities')
.whereIn('id', list)
.whereIn('user', users);
}
console.log(
`📤 [GET /activities/byIds] trovate ${rows.length} attività durata=${Date.now() - start}ms`
);
res.json(rows.map(normalizeActivity));
} catch (err) {
console.error("❌ Errore /activities/byIds:", err);
res.status(500).json({ error: err.message });
}
});
// -----------------------------------------------------
// GET /activities
// Full sync
// -----------------------------------------------------
router.get('/', async (req, res) => {
const loggedUser = req.user?.name;
const start = Date.now();
console.log(
`📤 [FULL SYNC] GET /activities da=${loggedUser} at=${new Date().toISOString()}`
);
try {
let rows;
if (loggedUser === "Admin") {
rows = await db('activities')
.select('*')
.orderBy('start_time_millis', 'desc');
} else {
rows = await db('activities')
.whereIn('user', [loggedUser, "Common"])
.orderBy('start_time_millis', 'desc');
}
console.log(
`📤 [FULL SYNC] Risposta: ${rows.length} attività durata=${Date.now() - start}ms`
);
res.json(rows.map(normalizeActivity));
} catch (err) {
console.error("❌ Errore GET /activities:", err);
res.status(500).json({ error: err.message });
}
});
// -----------------------------------------------------
// POST /activities/initDB
// Reset DB attività
// - Admin: cancella tutte le attività
// - Utente normale: cancella solo le proprie
// -----------------------------------------------------
router.post('/initDB', async (req, res) => {
try {
const loggedUser = req.user?.name;
if (!loggedUser) {
return res.status(401).json({
ok: false,
error: "Unauthorized"
});
}
console.log(`🧹 [POST /activities/initDB] richiesto da=${loggedUser}`);
if (loggedUser === "Admin") {
console.log("🧹 Admin → cancello tutte le attività");
await db('activities').del();
await db('deleted_hard_activities').del();
await db('sync_done_activities').del();
return res.json({
ok: true,
scope: "all",
user: loggedUser,
message: "DB attività svuotato completamente"
});
}
console.log(`🧹 User=${loggedUser} → cancello solo le sue attività`);
await db('activities')
.where({ user: loggedUser })
.del();
await db('deleted_hard_activities')
.where({ user: loggedUser })
.del();
await db('sync_done_activities').del();
res.json({
ok: true,
scope: "user",
user: loggedUser,
message: `DB attività svuotato per user=${loggedUser}`
});
} catch (err) {
console.error("❌ Errore POST /activities/initDB:", err);
res.status(500).json({
ok: false,
error: err.message
});
}
});
// -----------------------------------------------------
// POST /activities/initDBuser
// Reset DB attività per utente specifico
// Solo Admin
// -----------------------------------------------------
router.post('/initDBuser', async (req, res) => {
try {
const loggedUser = req.user?.name;
if (loggedUser !== "Admin") {
return res.status(403).json({
ok: false,
error: "Forbidden",
loggedUser
});
}
const targetUser =
req.body?.user ||
req.query?.user ||
req.body?.name ||
req.query?.name;
if (!targetUser) {
return res.status(400).json({
ok: false,
error: "Missing user"
});
}
console.log(
`🧹 [POST /activities/initDBuser] Admin=${loggedUser} target=${targetUser}`
);
await db('activities')
.where({ user: targetUser })
.del();
await db('deleted_hard_activities')
.where({ user: targetUser })
.del();
await db('sync_done_activities').del();
res.json({
ok: true,
user: targetUser,
message: `DB attività svuotato per user=${targetUser}`
});
} catch (err) {
console.error("❌ Errore POST /activities/initDBuser:", err);
res.status(500).json({
ok: false,
error: err.message
});
}
});
// -----------------------------------------------------
// GET /activities/deleted_hard
// Deve stare PRIMA di /:id
// -----------------------------------------------------
router.get('/deleted_hard', async (req, res) => {
const since = req.query.since;
const user = req.user.name;
try {
let rows;
if (user === "Admin") {
rows = await db('deleted_hard_activities')
.where('deleted_at', '>', since);
} else {
rows = await db('deleted_hard_activities')
.where('deleted_at', '>', since)
.where({ user });
}
res.json({ deleted: rows });
} catch (err) {
console.error("❌ Errore GET /activities/deleted_hard:", err);
res.status(500).json({ error: err.message });
}
});
// -----------------------------------------------------
// POST /activities/sync_done
// -----------------------------------------------------
router.post('/sync_done', async (req, res) => {
try {
await db('sync_done_activities').insert({
session_id: req.body.session_id,
device_id: req.body.device_id,
last_sync: req.body.last_sync
});
res.json({ ok: true });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// -----------------------------------------------------
// POST /activities/:id/toggle-soft
// Toggle soft delete attività per ID
// - Se deleted_at è null → soft delete
// - Se deleted_at è valorizzato → restore
// -----------------------------------------------------
router.post('/:id/toggle-soft', async (req, res) => {
const id = req.params.id;
const loggedUser = req.user?.name;
const start = Date.now();
try {
if (!loggedUser) {
return res.status(401).json({
ok: false,
error: "Unauthorized"
});
}
console.log(`🔁 [TOGGLE SOFT /activities/${id}] richiesto da=${loggedUser}`);
let row;
if (loggedUser === "Admin") {
row = await db('activities')
.where({ id })
.first();
} else {
row = await db('activities')
.where({ id, user: loggedUser })
.first();
}
if (!row) {
return res.status(404).json({
ok: false,
error: "Activity not found or not allowed",
id
});
}
const now = new Date().toISOString();
const nowMs = Date.now();
const nextDeletedAt = row.deleted_at ? null : now;
await db('activities')
.where({ id })
.update({
deleted_at: nextDeletedAt,
updated_at: now,
date_modified_millis: nowMs
});
try {
wss.broadcastToUser(row.user, {
type: "updated",
id,
entity: "activity",
deleted_at: nextDeletedAt
});
} catch (e) {
console.warn("⚠️ WS broadcast activity toggle soft fallito:", e.message);
}
console.log(
`✅ [TOGGLE SOFT /activities/${id}] user=${row.user} soft_deleted=${!!nextDeletedAt} durata=${Date.now() - start}ms`
);
res.json({
ok: true,
id,
user: row.user,
deleted_at: nextDeletedAt,
soft_deleted: !!nextDeletedAt,
restored: !nextDeletedAt,
duration_ms: Date.now() - start
});
} catch (err) {
console.error("❌ Errore POST /activities/:id/toggle-soft:", err);
res.status(500).json({
ok: false,
error: err.message
});
}
});
// -----------------------------------------------------
// DELETE /activities/:id
// HARD DELETE attività per ID
// - Sposta originale in public/del_activities/<user>/original/<cartella>/<attività>
// - Rimuove record da activities
// - Registra ID in deleted_hard_activities
// -----------------------------------------------------
router.delete('/:id', async (req, res) => {
const id = req.params.id;
const loggedUser = req.user?.name;
const start = Date.now();
try {
if (!loggedUser) {
return res.status(401).json({
ok: false,
error: "Unauthorized"
});
}
console.log(`🗑️ [HARD DELETE /activities/${id}] richiesto da=${loggedUser}`);
let row;
if (loggedUser === "Admin") {
row = await db('activities')
.where({ id })
.first();
} else {
row = await db('activities')
.where({ id, user: loggedUser })
.first();
}
if (!row) {
return res.status(404).json({
ok: false,
error: "Activity not found or not allowed",
id
});
}
const deletedAt = new Date().toISOString();
// 1. Sposta file originale attività
const originalSrc = getOriginalActivityPath(row);
const originalDst = getDeletedActivityPath(row);
const originalMoved = await moveFileSafe(originalSrc, originalDst);
// 2. Elimina record da activities
await db('activities')
.where({ id })
.del();
// 3. Registra hard delete
await db('deleted_hard_activities')
.insert({
id,
user: row.user,
deleted_at: deletedAt
})
.onConflict('id')
.merge();
// 4. WS realtime
try {
wss.broadcastToUser(row.user, {
type: "removed",
id,
entity: "activity"
});
} catch (e) {
console.warn("⚠️ WS broadcast activity hard delete fallito:", e.message);
}
console.log(
`✅ [HARD DELETE /activities/${id}] user=${row.user} durata=${Date.now() - start}ms`
);
res.json({
ok: true,
id,
user: row.user,
deleted_at: deletedAt,
hard_deleted: true,
files: {
original_src: originalSrc,
original_dst: originalDst,
original_moved: originalMoved
},
duration_ms: Date.now() - start
});
} catch (err) {
console.error("❌ Errore HARD DELETE /activities/:id:", err);
res.status(500).json({
ok: false,
error: err.message
});
}
});
// -----------------------------------------------------
// GET /activities/:id
// Deve stare DOPO deleted_hard, changes, byIds, initDB,
// sync_done, toggle-soft e delete.
// -----------------------------------------------------
router.get('/:id', async (req, res) => {
const id = req.params.id;
const loggedUser = req.user?.name;
const start = Date.now();
console.log(
`📤 [GET /activities/${id}] Richiesta da=${loggedUser} at=${new Date().toISOString()}`
);
try {
let rows;
if (loggedUser === "Admin") {
rows = await db('activities').where({ id });
} else {
rows = await db('activities')
.where({ id })
.whereIn('user', [loggedUser, "Common"]);
}
console.log(
`📤 [GET /activities/${id}] Risposta: ${rows.length} attività durata=${Date.now() - start}ms`
);
if (!rows.length) return res.json([]);
res.json(rows.map(normalizeActivity));
} catch (err) {
console.error("❌ Errore GET /activities/:id:", err);
res.status(500).json({ error: err.message });
}
});
module.exports = router;