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:
parent
ab83be605e
commit
c6b5767d5d
3
.gitignore
vendored
3
.gitignore
vendored
@ -24,4 +24,5 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
test-files/
|
||||
uploads/
|
||||
uploads/
|
||||
cache/
|
||||
@ -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',
|
||||
|
||||
@ -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
168
docs/HASH_SYSTEM.md
Normal 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*
|
||||
Loading…
x
Reference in New Issue
Block a user