const express = require('express'); const multer = require('multer'); const cors = require('cors'); const path = require('path'); const fs = require('fs'); const sqlite3 = require('sqlite3').verbose(); const AdmZip = require('adm-zip'); const TargetValidator = require('./services/target_validator'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(express.json()); // Arborescence demandée const uploadDir = path.join(__dirname, 'uploads'); const imagesDir = path.join(uploadDir, 'images'); const dataDir = path.join(uploadDir, 'data'); const exportsDir = path.join(__dirname, 'exports'); // S'assurer que les dossiers existent [uploadDir, imagesDir, dataDir, exportsDir].forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } }); // Initialisation de la base de données SQLite const dbPath = path.join(dataDir, 'database.sqlite'); const db = new sqlite3.Database(dbPath, (err) => { if (err) { console.error('Erreur de connexion à SQLite:', err.message); } else { console.log('Connecté à la base de données SQLite.'); db.run(`CREATE TABLE IF NOT EXISTS user_stats ( wallet_hash TEXT PRIMARY KEY, photo_count INTEGER DEFAULT 0, last_upload DATETIME DEFAULT CURRENT_TIMESTAMP )`); db.run(`CREATE TABLE IF NOT EXISTS banned_wallets ( wallet_hash TEXT PRIMARY KEY, reason TEXT, banned_at DATETIME DEFAULT CURRENT_TIMESTAMP, banned_by TEXT DEFAULT 'Admin' )`); db.run(`CREATE TABLE IF NOT EXISTS upload_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, session_id TEXT, wallet_hash TEXT, image_filename TEXT, json_filename TEXT, file_size INTEGER, ip_address TEXT, user_agent TEXT, device_model TEXT, device_os TEXT, target_type TEXT, weapon TEXT, distance_meters INTEGER, impacts_count INTEGER DEFAULT 0, status TEXT DEFAULT 'SUCCESS', target_valid INTEGER DEFAULT 1, target_status TEXT DEFAULT 'VALID', target_confidence REAL DEFAULT 1.0, target_rings_count INTEGER DEFAULT 0, target_details TEXT, error_message TEXT, raw_metadata TEXT )`); // Migration safe des colonnes OpenCV si la table existait déjà const migrations = [ "ALTER TABLE upload_logs ADD COLUMN target_valid INTEGER DEFAULT 1", "ALTER TABLE upload_logs ADD COLUMN target_status TEXT DEFAULT 'VALID'", "ALTER TABLE upload_logs ADD COLUMN target_confidence REAL DEFAULT 1.0", "ALTER TABLE upload_logs ADD COLUMN target_rings_count INTEGER DEFAULT 0", "ALTER TABLE upload_logs ADD COLUMN target_details TEXT" ]; migrations.forEach(sql => db.run(sql, () => {})); db.run(`CREATE INDEX IF NOT EXISTS idx_upload_logs_wallet ON upload_logs(wallet_hash)`); db.run(`CREATE INDEX IF NOT EXISTS idx_upload_logs_timestamp ON upload_logs(timestamp)`); } }); // Helper pour insérer un log d'upload function logUploadEntry({ sessionId = null, walletHash = null, imageFilename = null, jsonFilename = null, fileSize = null, ipAddress = null, userAgent = null, deviceModel = null, deviceOs = null, targetType = null, weapon = null, distanceMeters = null, impactsCount = 0, status = 'SUCCESS', targetValid = 1, targetStatus = 'VALID', targetConfidence = 1.0, targetRingsCount = 0, targetDetails = null, errorMessage = null, rawMetadata = null }) { const query = ` INSERT INTO upload_logs ( session_id, wallet_hash, image_filename, json_filename, file_size, ip_address, user_agent, device_model, device_os, target_type, weapon, distance_meters, impacts_count, status, target_valid, target_status, target_confidence, target_rings_count, target_details, error_message, raw_metadata ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `; db.run(query, [ sessionId, walletHash, imageFilename, jsonFilename, fileSize, ipAddress, userAgent, deviceModel, deviceOs, targetType, weapon, distanceMeters, impactsCount, status, targetValid ? 1 : 0, targetStatus, targetConfidence, targetRingsCount, targetDetails, errorMessage, rawMetadata ? JSON.stringify(rawMetadata) : null ], function(err) { if (err) { console.error("Erreur lors de l'insertion dans upload_logs:", err.message); } else { console.log(`[LOG] Upload enregistré (ID: ${this.lastID}) - Statut: ${status} - OpenCV: ${targetStatus}`); } }); } // Configuration de multer pour le stockage des fichiers const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, imagesDir); // Les photos vont dans uploads/images/ }, filename: function (req, file, cb) { // Créer un nom de fichier unique tout en gardant l'extension originale const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); const ext = path.extname(file.originalname); cb(null, file.fieldname + '-' + uniqueSuffix + ext); } }); const upload = multer({ storage: storage, limits: { fileSize: 15 * 1024 * 1024 // Limite de taille: 15 MB }, fileFilter: (req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase(); const allowedExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp']; // Accepter si c'est un mimetype image OU si l'extension est celle d'une image if (file.mimetype.startsWith('image/') || allowedExts.includes(ext) || file.mimetype === 'application/octet-stream') { cb(null, true); } else { console.error(`Fichier rejeté: mimetype=${file.mimetype}, extension=${ext}`); cb(new Error('Seules les images sont autorisées !'), false); } } }); // Routes de base app.get('/api/health', (req, res) => { res.json({ status: 'ok', message: 'Le serveur backend IA fonctionne' }); }); // Route pour l'upload de photo + données JSON // Attend un form-data avec un champ nommé 'photo' et un champ texte 'plotting' app.post('/api/upload', upload.single('photo'), async (req, res) => { const ipAddress = req.headers['x-forwarded-for'] || req.socket.remoteAddress || req.ip || ''; const userAgent = req.headers['user-agent'] || ''; try { if (!req.file) { logUploadEntry({ ipAddress, userAgent, status: 'FAILED', errorMessage: 'Aucune photo fournie' }); return res.status(400).json({ code: 'MISSING_PHOTO', error: 'Aucune photo fournie dans la requête' }); } let plottingData = {}; if (req.body.plotting) { try { plottingData = JSON.parse(req.body.plotting); } catch (e) { console.error("Erreur parsing JSON:", e); logUploadEntry({ imageFilename: req.file.filename, fileSize: req.file.size, ipAddress, userAgent, status: 'FAILED', errorMessage: 'Le champ plotting doit être un JSON valide' }); return res.status(400).json({ code: 'INVALID_PLOTTING_JSON', error: 'Le champ plotting contient un format JSON invalide' }); } } const walletHash = plottingData.wallet_hash || null; const sessionId = plottingData.session_id || null; // 1. Vérification du bannissement de wallet if (walletHash) { const bannedEntry = await new Promise((resolve) => { db.get('SELECT * FROM banned_wallets WHERE wallet_hash = ?', [walletHash], (err, row) => { resolve(row || null); }); }); if (bannedEntry) { // Supprimer le fichier image temporaire pour ne pas consommer d'espace if (req.file.path && fs.existsSync(req.file.path)) { try { fs.unlinkSync(req.file.path); } catch (e) {} } const banReason = bannedEntry.reason || 'Non-respect des règles de contribution / Image non conforme'; logUploadEntry({ sessionId, walletHash, imageFilename: req.file.filename, fileSize: req.file.size, ipAddress, userAgent, status: 'FAILED', errorMessage: `Upload bloqué : wallet banni (${banReason})` }); console.warn(`[MODÉRATION] Upload rejeté pour wallet banni: ${walletHash}`); return res.status(403).json({ code: 'WALLET_BANNED', error: 'Votre participation au programme d\'entraînement IA a été suspendue par la modération.', reason: banReason, banned_at: bannedEntry.banned_at }); } } // 2. Validation de la cible par OpenCV let validation = { isValid: true, status: 'VALID', confidence: 1.0, ringsCount: 0, bestCenter: null, details: 'Analyse non effectuée' }; try { validation = await TargetValidator.validateTarget(req.file.path); console.log(`[OPENCV] Diagnostic cible pour ${req.file.filename}: ${validation.status} (${Math.round(validation.confidence * 100)}% conf, ${validation.ringsCount} anneaux)`); } catch (cvErr) { console.error("[OPENCV] Erreur analyse cible:", cvErr); } // Nom de base sans l'extension const baseFilename = path.parse(req.file.filename).name; const jsonFilename = `${baseFilename}.json`; const jsonFilePath = path.join(dataDir, jsonFilename); // Sauvegarde du JSON dans uploads/data/ fs.writeFileSync(jsonFilePath, JSON.stringify(plottingData, null, 2)); // Extraction des métadonnées const deviceInfo = plottingData.device_info || {}; const targetMeta = plottingData.target_metadata || {}; const impacts = plottingData.plotting?.impacts || []; const impactsCount = Array.isArray(impacts) ? impacts.length : 0; // Mise à jour de la BDD si on a un wallet_hash if (walletHash) { db.run(` INSERT INTO user_stats (wallet_hash, photo_count, last_upload) VALUES (?, 1, CURRENT_TIMESTAMP) ON CONFLICT(wallet_hash) DO UPDATE SET photo_count = photo_count + 1, last_upload = CURRENT_TIMESTAMP `, [walletHash], function(err) { if (err) { console.error("Erreur lors de la mise à jour des stats :", err); } else { console.log(`Stats mises à jour pour le wallet_hash ${walletHash.substring(0, 8)}...`); } }); } // Enregistrement dans la table des logs d'upload avec diagnostic OpenCV logUploadEntry({ sessionId, walletHash, imageFilename: req.file.filename, jsonFilename, fileSize: req.file.size, ipAddress, userAgent, deviceModel: deviceInfo.model || null, deviceOs: deviceInfo.os || null, targetType: targetMeta.type || null, weapon: targetMeta.weapon || null, distanceMeters: targetMeta.distance_meters || null, impactsCount, status: 'SUCCESS', targetValid: validation.isValid ? 1 : 0, targetStatus: validation.status, targetConfidence: validation.confidence, targetRingsCount: validation.ringsCount, targetDetails: validation.details, rawMetadata: plottingData }); console.log(`Données reçues et sauvegardées:`); console.log(`- Image: uploads/images/${req.file.filename}`); console.log(`- JSON : uploads/data/${jsonFilename}`); res.status(200).json({ code: 'UPLOAD_SUCCESS', message: 'Photo et données uploadées avec succès', file: { filename: req.file.filename, url: `/uploads/images/${req.file.filename}`, size: req.file.size }, data: { filename: jsonFilename }, target_validation: validation }); } catch (error) { console.error('Erreur lors de l\'upload:', error); logUploadEntry({ imageFilename: req.file ? req.file.filename : null, fileSize: req.file ? req.file.size : null, ipAddress, userAgent, status: 'FAILED', errorMessage: error.message || 'Erreur interne lors de l\'upload' }); res.status(500).json({ code: 'SERVER_ERROR', error: error.message || 'Erreur interne du serveur lors de l\'upload' }); } }); // Routes de Modération : Gestion des bannissements de wallets app.get('/api/moderation/banned', (req, res) => { db.all('SELECT * FROM banned_wallets ORDER BY banned_at DESC', [], (err, rows) => { if (err) { console.error("Erreur lecture wallets bannis:", err); return res.status(500).json({ error: 'Erreur lecture wallets bannis' }); } res.json({ banned: rows || [] }); }); }); app.post('/api/moderation/ban', (req, res) => { const { wallet_hash, reason, banned_by } = req.body; if (!wallet_hash) { return res.status(400).json({ error: 'wallet_hash obligatoire' }); } db.run( `INSERT INTO banned_wallets (wallet_hash, reason, banned_by, banned_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT(wallet_hash) DO UPDATE SET reason = excluded.reason, banned_at = CURRENT_TIMESTAMP`, [wallet_hash, reason || 'Contenu invalide ou non conforme aux règles', banned_by || 'Admin'], function(err) { if (err) { console.error("Erreur bannissement:", err); return res.status(500).json({ error: 'Erreur lors du bannissement du wallet' }); } console.log(`[MODÉRATION] Wallet ${wallet_hash} banni (Motif: ${reason})`); res.json({ message: 'Wallet banni avec succès', wallet_hash }); } ); }); app.delete('/api/moderation/ban/:wallet_hash', (req, res) => { const wallet_hash = req.params.wallet_hash; db.run('DELETE FROM banned_wallets WHERE wallet_hash = ?', [wallet_hash], function(err) { if (err) { console.error("Erreur débannissement:", err); return res.status(500).json({ error: 'Erreur lors du débannissement' }); } console.log(`[MODÉRATION] Wallet ${wallet_hash} débanni`); res.json({ message: 'Wallet débanni avec succès', wallet_hash }); }); }); // Route pour relancer l'analyse OpenCV sur une photo déjà existante app.post('/api/moderation/verify/:filename', async (req, res) => { const filename = req.params.filename; const imgPath = path.join(imagesDir, filename); if (!fs.existsSync(imgPath)) { return res.status(404).json({ error: 'Image non trouvée' }); } try { const result = await TargetValidator.validateTarget(imgPath); res.json({ filename, validation: result }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Route pour lister les logs avec filtres, recherche et pagination app.get('/api/logs', (req, res) => { const limit = Math.min(Math.max(parseInt(req.query.limit) || 50, 1), 200); const offset = Math.max(parseInt(req.query.offset) || 0, 0); const wallet = req.query.wallet || req.query.wallet_hash; const status = req.query.status; const search = req.query.search; let whereClauses = []; let params = []; if (wallet) { whereClauses.push("wallet_hash LIKE ?"); params.push(`%${wallet}%`); } if (status) { whereClauses.push("status = ?"); params.push(status); } if (search) { whereClauses.push("(session_id LIKE ? OR wallet_hash LIKE ? OR device_model LIKE ? OR weapon LIKE ? OR target_type LIKE ? OR image_filename LIKE ?)"); const s = `%${search}%`; params.push(s, s, s, s, s, s); } const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : ''; db.get(`SELECT COUNT(*) as total FROM upload_logs ${whereSql}`, params, (err, countRow) => { if (err) { console.error("Erreur comptage logs:", err); return res.status(500).json({ error: 'Erreur lors du comptage des logs' }); } const total = countRow ? countRow.total : 0; const query = ` SELECT * FROM upload_logs ${whereSql} ORDER BY timestamp DESC, id DESC LIMIT ? OFFSET ? `; db.all(query, [...params, limit, offset], (err, rows) => { if (err) { console.error("Erreur lecture logs:", err); return res.status(500).json({ error: 'Erreur lors de la lecture des logs' }); } res.json({ logs: rows, total, limit, offset }); }); }); }); // Route pour les statistiques des logs app.get('/api/logs/stats', (req, res) => { const statsQuery = ` SELECT COUNT(*) as total_uploads, SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) as success_count, SUM(CASE WHEN status = 'FAILED' THEN 1 ELSE 0 END) as failed_count, SUM(CASE WHEN date(timestamp, 'localtime') = date('now', 'localtime') THEN 1 ELSE 0 END) as today_uploads, COUNT(DISTINCT wallet_hash) as unique_wallets, SUM(COALESCE(file_size, 0)) as total_bytes, MAX(timestamp) as latest_upload FROM upload_logs `; db.get(statsQuery, [], (err, stats) => { if (err) { console.error("Erreur stats logs:", err); return res.status(500).json({ error: 'Erreur calcul statistiques' }); } res.json({ status: 'ok', stats: stats || { total_uploads: 0, success_count: 0, failed_count: 0, today_uploads: 0, unique_wallets: 0, total_bytes: 0, latest_upload: null } }); }); }); // Route pour exporter les logs (CSV ou JSON) app.get('/api/logs/export', (req, res) => { const format = req.query.format || 'csv'; db.all('SELECT * FROM upload_logs ORDER BY timestamp DESC, id DESC', [], (err, rows) => { if (err) { console.error("Erreur export logs:", err); return res.status(500).json({ error: 'Erreur export logs' }); } if (format === 'json') { res.setHeader('Content-Disposition', `attachment; filename=upload_logs_${Date.now()}.json`); res.setHeader('Content-Type', 'application/json'); return res.send(JSON.stringify(rows, null, 2)); } // CSV Export const headers = [ 'ID', 'Date/Heure', 'Statut', 'Wallet Hash', 'Session ID', 'Image', 'JSON', 'Taille (octets)', 'Cible', 'Arme', 'Distance (m)', 'Impacts', 'Modèle Appareil', 'OS', 'IP', 'Erreur' ]; const csvLines = [headers.join(';')]; rows.forEach(r => { const line = [ r.id, `"${r.timestamp || ''}"`, `"${r.status || ''}"`, `"${r.wallet_hash || ''}"`, `"${r.session_id || ''}"`, `"${r.image_filename || ''}"`, `"${r.json_filename || ''}"`, r.file_size || 0, `"${r.target_type || ''}"`, `"${r.weapon || ''}"`, r.distance_meters || '', r.impacts_count || 0, `"${r.device_model || ''}"`, `"${r.device_os || ''}"`, `"${r.ip_address || ''}"`, `"${(r.error_message || '').replace(/"/g, '""')}"` ]; csvLines.push(line.join(';')); }); res.setHeader('Content-Disposition', `attachment; filename=upload_logs_${Date.now()}.csv`); res.setHeader('Content-Type', 'text/csv; charset=utf-8'); res.send('\uFEFF' + csvLines.join('\r\n')); }); }); // Route pour supprimer un log spécifique app.delete('/api/logs/:id', (req, res) => { const id = req.params.id; db.run('DELETE FROM upload_logs WHERE id = ?', [id], function(err) { if (err) { console.error("Erreur suppression log:", err); return res.status(500).json({ error: 'Erreur lors de la suppression' }); } res.json({ message: 'Log supprimé avec succès', deletedId: id }); }); }); // Route pour vider les logs app.delete('/api/logs', (req, res) => { db.run('DELETE FROM upload_logs', [], function(err) { if (err) { console.error("Erreur vidage logs:", err); return res.status(500).json({ error: 'Erreur lors de la réinitialisation des logs' }); } res.json({ message: 'Tous les logs ont été effacés', changes: this.changes }); }); }); // Route pour récupérer toutes les photos disponibles app.get('/api/photos', (req, res) => { try { fs.readdir(imagesDir, (err, files) => { if (err) { return res.status(500).json({ error: 'Impossible de lire le dossier des photos' }); } const photos = files.map(file => ({ filename: file, url: `/uploads/images/${file}` })); res.status(200).json({ photos }); }); } catch (error) { res.status(500).json({ error: 'Erreur serveur' }); } }); // Route pour récupérer les statistiques d'un wallet_hash + statut de modération app.get('/api/stats/:wallet_hash', (req, res) => { const walletHash = req.params.wallet_hash; db.get('SELECT photo_count, last_upload FROM user_stats WHERE wallet_hash = ?', [walletHash], (err, statRow) => { if (err) { return res.status(500).json({ error: 'Erreur lors de la lecture des statistiques' }); } db.get('SELECT reason, banned_at FROM banned_wallets WHERE wallet_hash = ?', [walletHash], (bErr, bannedRow) => { const isBanned = !!bannedRow; const stats = statRow || { photo_count: 0, last_upload: null }; res.json({ status: 'ok', stats, is_banned: isBanned, ban_reason: bannedRow ? bannedRow.reason : null, banned_at: bannedRow ? bannedRow.banned_at : null }); }); }); }); // Route directe pour vérifier l'état de modération d'un wallet app.get('/api/moderation/status/:wallet_hash', (req, res) => { const walletHash = req.params.wallet_hash; db.get('SELECT reason, banned_at FROM banned_wallets WHERE wallet_hash = ?', [walletHash], (err, bannedRow) => { if (err) { return res.status(500).json({ error: 'Erreur lecture statut modération' }); } res.json({ wallet_hash: walletHash, is_banned: !!bannedRow, ban_reason: bannedRow ? bannedRow.reason : null, banned_at: bannedRow ? bannedRow.banned_at : null }); }); }); // Route pour récupérer TOUS les contributeurs app.get('/api/contributors', (req, res) => { db.all('SELECT * FROM user_stats ORDER BY photo_count DESC', [], (err, rows) => { if (err) { return res.status(500).json({ error: 'Erreur lors de la lecture des contributeurs' }); } res.json({ contributors: rows }); }); }); // Route pour récupérer toutes les données (Photos + JSON) app.get('/api/all-data', (req, res) => { try { fs.readdir(imagesDir, (err, files) => { if (err) { return res.status(500).json({ error: 'Impossible de lire le dossier des photos' }); } const allData = files.map(file => { const baseName = path.parse(file).name; const jsonPath = path.join(dataDir, `${baseName}.json`); let plotting = null; if (fs.existsSync(jsonPath)) { try { plotting = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); } catch (e) { console.error(`Erreur lecture JSON pour ${file}`); } } return { filename: file, imageUrl: `/uploads/images/${file}`, data: plotting }; }); res.status(200).json({ data: allData }); }); } catch (error) { res.status(500).json({ error: 'Erreur serveur' }); } }); // Route pour mettre à jour les données JSON d'une session app.put('/api/data/:filename', (req, res) => { const filename = req.params.filename; const baseName = path.parse(filename).name; const jsonPath = path.join(dataDir, `${baseName}.json`); try { if (!fs.existsSync(jsonPath)) { return res.status(404).json({ error: 'Fichier JSON non trouvé' }); } // On écrase le fichier avec les nouvelles données reçues const newData = req.body; fs.writeFileSync(jsonPath, JSON.stringify(newData, null, 2)); console.log(`Données mises à jour pour : ${baseName}.json`); res.status(200).json({ message: 'Données mises à jour avec succès' }); } catch (error) { console.error('Erreur lors de la mise à jour du JSON:', error); res.status(500).json({ error: 'Erreur serveur lors de la mise à jour' }); } }); // Route pour exporter toutes les données en ZIP app.get('/api/export/zip', (req, res) => { try { const zip = new AdmZip(); // Ajouter les dossiers au ZIP if (fs.existsSync(imagesDir)) zip.addLocalFolder(imagesDir, 'images'); if (fs.existsSync(dataDir)) zip.addLocalFolder(dataDir, 'data'); const zipFilename = `dataset_export_${Date.now()}.zip`; const zipPath = path.join(exportsDir, zipFilename); // Sauvegarder sur le disque zip.writeZip(zipPath); // Envoyer le fichier au client res.download(zipPath, zipFilename, (err) => { if (err) console.error("Erreur lors de l'envoi du ZIP:", err); // Optionnel: supprimer le ZIP local après envoi si vous voulez gagner de la place // fs.unlinkSync(zipPath); }); } catch (error) { console.error('Erreur lors de la création du ZIP:', error); res.status(500).json({ error: 'Erreur lors de la création de l\'archive' }); } }); // Route pour archiver et vider les dossiers uploads app.post('/api/export/archive', (req, res) => { try { const archiveName = `archive_${new Date().toISOString().replace(/[:.]/g, '-')}`; const archivePath = path.join(exportsDir, archiveName); if (!fs.existsSync(archivePath)) { fs.mkdirSync(archivePath, { recursive: true }); } // Fonction pour déplacer les fichiers const moveFiles = (source, destSubDir) => { const dest = path.join(archivePath, destSubDir); if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); const files = fs.readdirSync(source); files.forEach(file => { // Ne pas déplacer la base de données sqlite if (file === 'database.sqlite') return; const oldPath = path.join(source, file); const newPath = path.join(dest, file); try { fs.renameSync(oldPath, newPath); } catch (e) { // Fallback si renameSync échoue (ex: disque différent) fs.copyFileSync(oldPath, newPath); fs.unlinkSync(oldPath); } }); }; moveFiles(imagesDir, 'images'); moveFiles(dataDir, 'data'); res.json({ message: 'Données archivées avec succès', archive: archiveName }); } catch (error) { console.error('Erreur lors de l\'archivage:', error); res.status(500).json({ error: 'Erreur lors de l\'archivage des données' }); } }); // Servir les fichiers statiques (pour pouvoir accéder aux photos via URL) app.use('/uploads/images', express.static(imagesDir)); // Gestion des erreurs app.use((err, req, res, next) => { if (err instanceof multer.MulterError) { if (err.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: 'Le fichier est trop volumineux (limite 15MB)' }); } } console.error(err.stack); res.status(500).json({ error: err.message || 'Une erreur inattendue est survenue' }); }); // Démarrer le serveur app.listen(PORT, () => { console.log(`=================================`); console.log(`Serveur Backend IA démarré`); console.log(`Port: ${PORT}`); console.log(`Dossiers:`); console.log(` - Images: ${imagesDir}`); console.log(` - Data : ${dataDir}`); console.log(` - Export: ${exportsDir}`); console.log(`=================================`); });