From c6b5767d5da68159b80d6e0d2a583fac9e2d1dec Mon Sep 17 00:00:00 2001 From: Nicolas Cantu Date: Tue, 16 Sep 2025 02:01:38 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Impl=C3=A9mentation=20du=20syst=C3=A8me?= =?UTF-8?q?=20de=20cache=20JSON=20et=20de=20hash=20pour=20les=20uploads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout du système de hash SHA-256 pour éviter les doublons d'upload - Implémentation du cache JSON pour sauvegarder les résultats d'extraction - Nouvelles fonctions: calculateFileHash, findExistingFileByHash, saveJsonCache, getJsonCache - Nouvelles routes API: /api/cache, /api/cache/:hash, /api/uploads - Optimisation des performances: réutilisation des résultats en cache - Documentation mise à jour: API_BACKEND.md et nouveau fichier HASH_SYSTEM.md - Ajout du dossier cache/ au .gitignore --- .gitignore | 3 +- backend/server.js | 234 +++++++++++++++++++++++++++++++++++++++++++- docs/API_BACKEND.md | 44 ++++++++- docs/HASH_SYSTEM.md | 168 +++++++++++++++++++++++++++++++ 4 files changed, 443 insertions(+), 6 deletions(-) create mode 100644 docs/HASH_SYSTEM.md diff --git a/.gitignore b/.gitignore index 915efea..3f87d4b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ dist-ssr *.sln *.sw? test-files/ -uploads/ \ No newline at end of file +uploads/ +cache/ \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index c56a146..9cdf88e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -10,6 +10,7 @@ const multer = require('multer') const cors = require('cors') const path = require('path') const fs = require('fs') +const crypto = require('crypto') const { createWorker } = require('tesseract.js') const { preprocessImageForOCR, analyzeImageMetadata } = require('./imagePreprocessing') const pdf = require('pdf-parse') @@ -22,7 +23,92 @@ app.use(cors()) app.use(express.json()) app.use(express.static('public')) -// Configuration multer pour l'upload de fichiers +// Fonction pour calculer le hash d'un fichier +function calculateFileHash(buffer) { + return crypto.createHash('sha256').update(buffer).digest('hex') +} + +// Fonction pour vérifier si un fichier existe déjà par hash +function findExistingFileByHash(hash) { + const uploadDir = 'uploads/' + if (!fs.existsSync(uploadDir)) return null + + const files = fs.readdirSync(uploadDir) + for (const file of files) { + const filePath = path.join(uploadDir, file) + try { + const fileBuffer = fs.readFileSync(filePath) + const fileHash = calculateFileHash(fileBuffer) + if (fileHash === hash) { + return { path: filePath, name: file } + } + } catch (error) { + console.warn(`[HASH] Erreur lors de la lecture de ${file}:`, error.message) + } + } + return null +} + +// Fonction pour sauvegarder le cache JSON +function saveJsonCache(hash, result) { + const cacheDir = 'cache/' + if (!fs.existsSync(cacheDir)) { + fs.mkdirSync(cacheDir, { recursive: true }) + } + + const cacheFile = path.join(cacheDir, `${hash}.json`) + try { + fs.writeFileSync(cacheFile, JSON.stringify(result, null, 2)) + console.log(`[CACHE] Résultat sauvegardé: ${hash.substring(0, 16)}...`) + return true + } catch (error) { + console.error(`[CACHE] Erreur lors de la sauvegarde:`, error.message) + return false + } +} + +// Fonction pour récupérer le cache JSON +function getJsonCache(hash) { + const cacheFile = path.join('cache/', `${hash}.json`) + try { + if (fs.existsSync(cacheFile)) { + const cachedData = fs.readFileSync(cacheFile, 'utf8') + const result = JSON.parse(cachedData) + console.log(`[CACHE] Résultat récupéré: ${hash.substring(0, 16)}...`) + return result + } + } catch (error) { + console.warn(`[CACHE] Erreur lors de la lecture du cache:`, error.message) + } + return null +} + +// Fonction pour lister les fichiers de cache +function listCacheFiles() { + const cacheDir = 'cache/' + if (!fs.existsSync(cacheDir)) return [] + + const files = fs.readdirSync(cacheDir) + return files.map(file => { + const filePath = path.join(cacheDir, file) + try { + const stats = fs.statSync(filePath) + const hash = path.basename(file, '.json') + return { + hash: hash, + fileName: file, + size: stats.size, + createdDate: stats.birthtime, + modifiedDate: stats.mtime + } + } catch (error) { + console.warn(`[CACHE] Erreur lors de la lecture de ${file}:`, error.message) + return null + } + }).filter(file => file !== null) +} + +// Configuration multer pour l'upload de fichiers avec gestion des doublons const storage = multer.diskStorage({ destination: (req, file, cb) => { const uploadDir = 'uploads/' @@ -32,8 +118,11 @@ const storage = multer.diskStorage({ cb(null, uploadDir) }, filename: (req, file, cb) => { - const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9) - cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)) + // Utiliser le nom original avec timestamp pour éviter les conflits + const timestamp = Date.now() + const ext = path.extname(file.originalname) + const name = path.basename(file.originalname, ext) + cb(null, `${name}-${timestamp}${ext}`) } }) @@ -641,6 +730,42 @@ app.post('/api/extract', upload.single('document'), async (req, res) => { console.log(`[API] Traitement du fichier: ${req.file.originalname}`) + // Calculer le hash du fichier uploadé + const fileBuffer = fs.readFileSync(req.file.path) + const fileHash = calculateFileHash(fileBuffer) + console.log(`[HASH] Hash du fichier: ${fileHash.substring(0, 16)}...`) + + // Vérifier d'abord le cache JSON + const cachedResult = getJsonCache(fileHash) + if (cachedResult) { + console.log(`[CACHE] Utilisation du résultat en cache`) + + // Supprimer le fichier temporaire + fs.unlinkSync(req.file.path) + + // Retourner le résultat en cache + return res.json(cachedResult) + } + + // Vérifier si un fichier avec le même hash existe déjà + const existingFile = findExistingFileByHash(fileHash) + let isDuplicate = false + let duplicatePath = null + + if (existingFile) { + console.log(`[HASH] Fichier déjà existant trouvé: ${existingFile.name}`) + isDuplicate = true + + // Sauvegarder le chemin du doublon pour suppression ultérieure + duplicatePath = req.file.path + + // Utiliser le fichier existant pour le traitement + req.file.path = existingFile.path + req.file.originalname = existingFile.name + } else { + console.log(`[HASH] Nouveau fichier, traitement normal`) + } + let ocrResult // Si c'est un PDF, extraire le texte directement @@ -671,8 +796,17 @@ app.post('/api/extract', upload.single('document'), async (req, res) => { // Génération du format JSON standard const result = generateStandardJSON(req.file, ocrResult, entities, processingTime) + // Sauvegarder le résultat dans le cache + saveJsonCache(fileHash, result) + // Nettoyage du fichier temporaire - fs.unlinkSync(req.file.path) + if (isDuplicate) { + // Supprimer le doublon uploadé + fs.unlinkSync(duplicatePath) + } else { + // Supprimer le fichier temporaire normal + fs.unlinkSync(req.file.path) + } console.log(`[API] Traitement terminé avec succès - Confiance: ${Math.round(result.metadata.quality.globalConfidence * 100)}%`) @@ -721,6 +855,98 @@ app.get('/api/test-files', (req, res) => { }) // Route de santé +// Route pour lister les fichiers uploadés avec leurs hash +app.get('/api/uploads', (req, res) => { + try { + const uploadDir = 'uploads/' + if (!fs.existsSync(uploadDir)) { + return res.json({ files: [] }) + } + + const files = fs.readdirSync(uploadDir) + const fileList = files.map(file => { + const filePath = path.join(uploadDir, file) + try { + const stats = fs.statSync(filePath) + const fileBuffer = fs.readFileSync(filePath) + const hash = calculateFileHash(fileBuffer) + + return { + name: file, + size: stats.size, + hash: hash, + uploadDate: stats.birthtime, + modifiedDate: stats.mtime + } + } catch (error) { + console.warn(`[API] Erreur lors de la lecture de ${file}:`, error.message) + return null + } + }).filter(file => file !== null) + + res.json({ + files: fileList, + count: fileList.length, + totalSize: fileList.reduce((sum, file) => sum + file.size, 0) + }) + } catch (error) { + console.error('[API] Erreur lors de la liste des fichiers:', error) + res.status(500).json({ error: 'Erreur lors de la récupération des fichiers' }) + } +}) + +// Route pour lister les fichiers de cache JSON +app.get('/api/cache', (req, res) => { + try { + const cacheFiles = listCacheFiles() + + res.json({ + files: cacheFiles, + count: cacheFiles.length, + totalSize: cacheFiles.reduce((sum, file) => sum + file.size, 0) + }) + } catch (error) { + console.error('[API] Erreur lors de la liste du cache:', error) + res.status(500).json({ error: 'Erreur lors de la récupération du cache' }) + } +}) + +// Route pour récupérer un résultat de cache spécifique +app.get('/api/cache/:hash', (req, res) => { + try { + const { hash } = req.params + const cachedResult = getJsonCache(hash) + + if (cachedResult) { + res.json(cachedResult) + } else { + res.status(404).json({ error: 'Résultat non trouvé dans le cache' }) + } + } catch (error) { + console.error('[API] Erreur lors de la récupération du cache:', error) + res.status(500).json({ error: 'Erreur lors de la récupération du cache' }) + } +}) + +// Route pour supprimer un fichier de cache +app.delete('/api/cache/:hash', (req, res) => { + try { + const { hash } = req.params + const cacheFile = path.join('cache/', `${hash}.json`) + + if (fs.existsSync(cacheFile)) { + fs.unlinkSync(cacheFile) + console.log(`[CACHE] Fichier supprimé: ${hash.substring(0, 16)}...`) + res.json({ message: 'Fichier de cache supprimé avec succès' }) + } else { + res.status(404).json({ error: 'Fichier de cache non trouvé' }) + } + } catch (error) { + console.error('[API] Erreur lors de la suppression du cache:', error) + res.status(500).json({ error: 'Erreur lors de la suppression du cache' }) + } +}) + app.get('/api/health', (req, res) => { res.json({ status: 'OK', diff --git a/docs/API_BACKEND.md b/docs/API_BACKEND.md index bfee1d5..49c377e 100644 --- a/docs/API_BACKEND.md +++ b/docs/API_BACKEND.md @@ -11,6 +11,7 @@ L'API Backend 4NK_IA est un service d'extraction et d'analyse de documents utili - ✅ **NER intelligent** : Reconnaissance d'entités par règles - ✅ **Format JSON standardisé** : Structure cohérente pour tous les documents - ✅ **Préprocessing d'images** : Amélioration automatique de la qualité OCR +- ✅ **Gestion des doublons** : Système de hash SHA-256 pour éviter les fichiers dupliqués --- @@ -63,7 +64,42 @@ Retourne la liste des fichiers de test disponibles. --- -## 🔍 **3. Extraction de documents** +## 📂 **3. Liste des fichiers uploadés** + +### **GET** `/api/uploads` + +Retourne la liste des fichiers uploadés avec leurs métadonnées et hash SHA-256. + +**Réponse :** +```json +{ + "files": [ + { + "name": "document-1757980637671.pdf", + "size": 1015808, + "hash": "a1b2c3d4e5f6...", + "uploadDate": "2025-09-15T23:56:14.751Z", + "modifiedDate": "2025-09-15T23:56:14.751Z" + } + ], + "count": 1, + "totalSize": 1015808 +} +``` + +**Exemple d'utilisation :** +```bash +curl http://localhost:3001/api/uploads +``` + +**Notes :** +- Le hash SHA-256 permet d'identifier les fichiers identiques +- Les fichiers dupliqués sont automatiquement détectés lors de l'upload +- Seuls les fichiers uniques sont conservés dans le système + +--- + +## 🔍 **4. Extraction de documents** ### **POST** `/api/extract` @@ -73,6 +109,12 @@ Extrait et analyse un document (PDF ou image) pour identifier les entités et in - **`document`** (file, required) : Fichier à analyser (PDF, JPEG, PNG, TIFF) - **Taille maximale :** 10MB +#### **Gestion des doublons :** +- Le système calcule automatiquement un hash SHA-256 de chaque fichier uploadé +- Si un fichier avec le même hash existe déjà, le doublon est supprimé +- Le traitement utilise le fichier existant, évitant ainsi les calculs redondants +- Les logs indiquent clairement si un fichier est nouveau ou déjà existant + #### **Réponse - Format JSON Standard :** ```json diff --git a/docs/HASH_SYSTEM.md b/docs/HASH_SYSTEM.md new file mode 100644 index 0000000..d48e8ed --- /dev/null +++ b/docs/HASH_SYSTEM.md @@ -0,0 +1,168 @@ +# 🔐 Système de Hash pour la Gestion des Doublons + +## 📋 **Vue d'ensemble** + +Le système de hash SHA-256 a été implémenté dans le backend 4NK_IA pour éviter les doublons d'upload et optimiser le stockage des fichiers. Ce système garantit qu'un fichier identique ne sera traité qu'une seule fois, même s'il est uploadé plusieurs fois. + +--- + +## 🔧 **Fonctionnement** + +### **1. Calcul du Hash** +- Chaque fichier uploadé est analysé pour calculer son hash SHA-256 +- Le hash est calculé sur le contenu binaire complet du fichier +- Utilisation de la fonction `crypto.createHash('sha256')` de Node.js + +### **2. Détection des Doublons** +- Avant traitement, le système vérifie si un fichier avec le même hash existe déjà +- La fonction `findExistingFileByHash()` parcourt le dossier `uploads/` +- Si un doublon est trouvé, le fichier uploadé est supprimé + +### **3. Traitement Optimisé** +- Le traitement utilise le fichier existant (pas le doublon) +- Les résultats d'extraction sont identiques pour les fichiers identiques +- Économie de ressources CPU et de stockage + +--- + +## 📁 **Structure des Fichiers** + +``` +uploads/ +├── document-1757980637671.pdf # Fichier original +├── document-1757980640576.pdf # Doublon (supprimé après traitement) +└── image-1757980651234.jpg # Autre fichier unique +``` + +--- + +## 🔍 **API Endpoints** + +### **GET** `/api/uploads` +Liste tous les fichiers uploadés avec leurs métadonnées : + +```json +{ + "files": [ + { + "name": "document-1757980637671.pdf", + "size": 1015808, + "hash": "a1b2c3d4e5f6789...", + "uploadDate": "2025-09-15T23:56:14.751Z", + "modifiedDate": "2025-09-15T23:56:14.751Z" + } + ], + "count": 1, + "totalSize": 1015808 +} +``` + +--- + +## 📊 **Logs et Monitoring** + +### **Logs de Hash** +``` +[HASH] Hash du fichier: a1b2c3d4e5f6789... +[HASH] Fichier déjà existant trouvé: document-1757980637671.pdf +[HASH] Nouveau fichier, traitement normal +``` + +### **Indicateurs de Performance** +- **Temps de traitement réduit** pour les doublons +- **Stockage optimisé** (pas de fichiers redondants) +- **Logs clairs** pour le debugging + +--- + +## 🛠️ **Fonctions Techniques** + +### **`calculateFileHash(buffer)`** +```javascript +function calculateFileHash(buffer) { + return crypto.createHash('sha256').update(buffer).digest('hex') +} +``` + +### **`findExistingFileByHash(hash)`** +```javascript +function findExistingFileByHash(hash) { + const uploadDir = 'uploads/' + if (!fs.existsSync(uploadDir)) return null + + const files = fs.readdirSync(uploadDir) + for (const file of files) { + const filePath = path.join(uploadDir, file) + try { + const fileBuffer = fs.readFileSync(filePath) + const fileHash = calculateFileHash(fileBuffer) + if (fileHash === hash) { + return { path: filePath, name: file } + } + } catch (error) { + console.warn(`[HASH] Erreur lors de la lecture de ${file}:`, error.message) + } + } + return null +} +``` + +--- + +## ✅ **Avantages** + +1. **Économie de Stockage** : Pas de fichiers dupliqués +2. **Performance** : Traitement plus rapide pour les doublons +3. **Cohérence** : Résultats identiques pour fichiers identiques +4. **Monitoring** : Visibilité sur les fichiers stockés +5. **Sécurité** : Hash SHA-256 cryptographiquement sûr + +--- + +## 🔄 **Workflow d'Upload** + +```mermaid +graph TD + A[Fichier Uploadé] --> B[Calcul Hash SHA-256] + B --> C{Hash Existe?} + C -->|Oui| D[Supprimer Doublon] + C -->|Non| E[Conserver Fichier] + D --> F[Utiliser Fichier Existant] + E --> G[Traitement Normal] + F --> H[Extraction & NER] + G --> H + H --> I[Résultat JSON] +``` + +--- + +## 🧪 **Tests** + +### **Test de Doublon** +```bash +# Premier upload +curl -X POST -F "document=@test.pdf" http://localhost:3001/api/extract + +# Deuxième upload du même fichier +curl -X POST -F "document=@test.pdf" http://localhost:3001/api/extract +``` + +### **Vérification** +```bash +# Lister les fichiers uploadés +curl http://localhost:3001/api/uploads +``` + +--- + +## 📝 **Notes d'Implémentation** + +- **Algorithme** : SHA-256 (256 bits) +- **Performance** : O(n) pour la recherche de doublons +- **Stockage** : Hash stocké en hexadécimal (64 caractères) +- **Compatibilité** : Fonctionne avec tous les types de fichiers supportés +- **Nettoyage** : Suppression automatique des doublons temporaires + +--- + +*Documentation mise à jour le 15 septembre 2025*