Files
demat-facturation/server/localStorage.ts
Manus 5a01860aba Checkpoint: Application complète de dématérialisation de facturation avec extraction IA (Mistral), authentification locale + Azure AD, stockage local, et export SFTP.
Fonctionnalités implémentées :
 Authentification locale (email/password) + Azure AD + Manus OAuth
 Upload drag-and-drop de fichiers PDF avec suivi en temps réel
 Extraction automatique avec Mistral AI (OCR + LLM)
 Détection de doublons (fournisseur, numéro, date)
 Score de qualité d'extraction (0-100)
 Tableau de bord avec statistiques
 Liste des factures avec recherche et filtres
 Paramètres utilisateur (LLM, keywords, SFTP)
 Historique des imports avec logs détaillés
 Gestion des utilisateurs (admin)
 Export SFTP manuel/automatique
 Stockage local avec organisation YYYY-MM
 Tests unitaires d'authentification

Architecture :
- Frontend : React 19 + Vite + TailwindCSS + Radix UI
- Backend : Express + tRPC + Drizzle ORM
- Base de données : MySQL (6 tables)
- IA : Mistral AI pour extraction
- Stockage : Local filesystem
- Export : SFTP

Pages :
- Login (choix local/Azure/Manus)
- Dashboard (statistiques)
- Upload (drag-and-drop)
- Invoices (liste avec recherche)
- Settings (LLM, keywords, SFTP)
- History (logs d'import)
- Users (gestion admin)
2026-01-08 06:02:07 -05:00

112 lines
3.1 KiB
TypeScript

import fs from "fs/promises";
import path from "path";
import { nanoid } from "nanoid";
// Storage base path (local filesystem)
const STORAGE_BASE_PATH = process.env.STORAGE_BASE_PATH || path.join(process.cwd(), "storage");
/**
* Ensure storage directory exists
*/
async function ensureStorageDir(dirPath: string) {
try {
await fs.mkdir(dirPath, { recursive: true });
} catch (error) {
console.error(`[LocalStorage] Failed to create directory ${dirPath}:`, error);
throw error;
}
}
/**
* Generate a storage key with YYYY-MM prefix for organization
*/
export function generateStorageKey(userId: number, fileName: string): string {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const randomId = nanoid(8);
// Format: YYYY-MM/userId-randomId-filename
return `${year}-${month}/${userId}-${randomId}-${fileName}`;
}
/**
* Store a file in local storage
* @param fileKey - Storage key (e.g., "2025-01/1-abc123-invoice.pdf")
* @param buffer - File content as Buffer
* @param contentType - MIME type (optional, for metadata)
* @returns Object with key and public URL
*/
export async function localStoragePut(
fileKey: string,
buffer: Buffer,
contentType?: string
): Promise<{ key: string; url: string }> {
try {
const fullPath = path.join(STORAGE_BASE_PATH, fileKey);
const dirPath = path.dirname(fullPath);
// Ensure directory exists
await ensureStorageDir(dirPath);
// Write file
await fs.writeFile(fullPath, buffer);
// Generate public URL (served by Express static middleware)
const url = `/storage/${fileKey}`;
console.log(`[LocalStorage] File stored: ${fileKey}`);
return { key: fileKey, url };
} catch (error) {
console.error(`[LocalStorage] Failed to store file ${fileKey}:`, error);
throw error;
}
}
/**
* Retrieve a file from local storage
* @param fileKey - Storage key
* @returns File content as Buffer
*/
export async function localStorageGet(fileKey: string): Promise<Buffer> {
try {
const fullPath = path.join(STORAGE_BASE_PATH, fileKey);
const buffer = await fs.readFile(fullPath);
return buffer;
} catch (error) {
console.error(`[LocalStorage] Failed to retrieve file ${fileKey}:`, error);
throw error;
}
}
/**
* Delete a file from local storage
* @param fileKey - Storage key
*/
export async function localStorageDelete(fileKey: string): Promise<void> {
try {
const fullPath = path.join(STORAGE_BASE_PATH, fileKey);
await fs.unlink(fullPath);
console.log(`[LocalStorage] File deleted: ${fileKey}`);
} catch (error) {
console.error(`[LocalStorage] Failed to delete file ${fileKey}:`, error);
throw error;
}
}
/**
* Check if a file exists in storage
* @param fileKey - Storage key
* @returns true if file exists, false otherwise
*/
export async function localStorageExists(fileKey: string): Promise<boolean> {
try {
const fullPath = path.join(STORAGE_BASE_PATH, fileKey);
await fs.access(fullPath);
return true;
} catch {
return false;
}
}