feat: Implémentation du système de cache JSON et de hash pour les uploads

- 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
This commit is contained in:
Nicolas Cantu 2025-09-16 02:01:38 +02:00
parent ab83be605e
commit c6b5767d5d
4 changed files with 443 additions and 6 deletions

3
.gitignore vendored
View File

@ -24,4 +24,5 @@ dist-ssr
*.sln
*.sw?
test-files/
uploads/
uploads/
cache/

View File

@ -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',

View File

@ -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

168
docs/HASH_SYSTEM.md Normal file
View File

@ -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*