espace-paie-odentas/lib/odentas-sign/cloudinary.ts
odentas 59749d481b feat: Migration Cloudinary vers Poppler pour conversion PDF→JPEG
- Remplacer Cloudinary (US) par solution 100% AWS eu-west-3
- Lambda odentas-sign-pdf-converter avec pdftoppm
- Lambda Layer poppler-utils v5 avec dépendances complètes
- Trigger S3 ObjectCreated pour conversion automatique
- Support multi-pages validé (PDF 3 pages)
- Stockage images dans S3 odentas-docs
- PDFImageViewer pour affichage images converties
- Conformité RGPD garantie (données EU uniquement)
2025-10-28 10:22:45 +01:00

218 lines
6.7 KiB
TypeScript

import { v2 as cloudinary } from 'cloudinary';
import { S3Client, PutObjectCommand, GetObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
// Configuration Cloudinary
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
secure: true,
});
// Client S3 pour stocker les images converties
const s3Client = new S3Client({
region: process.env.AWS_REGION || 'eu-west-3',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
const S3_BUCKET = process.env.AWS_S3_BUCKET || 'odentas-docs';
interface PageImage {
pageNumber: number;
imageUrl: string;
width: number;
height: number;
}
/**
* Vérifie si les images d'un PDF existent déjà dans S3
*/
async function checkImagesExistInS3(requestId: string): Promise<PageImage[] | null> {
try {
// On essaie de trouver les images existantes
// On commence par vérifier la page 1
const firstPageKey = `odentas-sign-images/${requestId}/page-1.jpg`;
try {
await s3Client.send(new HeadObjectCommand({
Bucket: S3_BUCKET,
Key: firstPageKey,
}));
} catch (error) {
// La première page n'existe pas, donc les images ne sont pas en cache
return null;
}
console.log('[S3 Cache] Images trouvées dans S3, récupération...');
// La page 1 existe, on récupère toutes les pages
const pageImages: PageImage[] = [];
let pageNum = 1;
while (true) {
const s3Key = `odentas-sign-images/${requestId}/page-${pageNum}.jpg`;
try {
// Vérifier si la page existe
await s3Client.send(new HeadObjectCommand({
Bucket: S3_BUCKET,
Key: s3Key,
}));
// Générer l'URL presignée
const command = new GetObjectCommand({
Bucket: S3_BUCKET,
Key: s3Key,
});
const s3Url = await getSignedUrl(s3Client, command, { expiresIn: 86400 });
pageImages.push({
pageNumber: pageNum,
imageUrl: s3Url,
width: 1400,
height: Math.round(1400 * 1.414),
});
pageNum++;
} catch (error) {
// Plus de pages, on sort de la boucle
break;
}
}
console.log(`[S3 Cache] ✅ ${pageImages.length} page(s) récupérées depuis S3 (pas de conversion)`);
return pageImages.length > 0 ? pageImages : null;
} catch (error) {
console.log('[S3 Cache] Pas de cache trouvé, conversion nécessaire');
return null;
}
}
/**
* Convertit un PDF en images JPEG via Cloudinary,
* puis stocke les images sur S3 pour économiser la bande passante Cloudinary
*/
export async function convertPdfToImagesWithCloudinary(
pdfBuffer: Buffer,
requestId: string
): Promise<PageImage[]> {
try {
// 1. Vérifier si les images existent déjà dans S3
const cachedImages = await checkImagesExistInS3(requestId);
if (cachedImages) {
console.log('[S3 Cache] ✅ Utilisation du cache S3, pas de conversion Cloudinary');
return cachedImages;
}
console.log('[Cloudinary] Pas de cache S3, début conversion...');
console.log('[Cloudinary] Début upload PDF, taille:', pdfBuffer.length);
// 1. Upload le PDF sur Cloudinary pour conversion
const uploadResult = await new Promise<any>((resolve, reject) => {
const uploadStream = cloudinary.uploader.upload_stream(
{
folder: `odentas-sign-temp/${requestId}`,
resource_type: 'image',
format: 'jpg',
public_id: `pdf-${requestId}`,
transformation: [
{ width: 1400, crop: 'scale' },
{ quality: 90 },
],
},
(error, result) => {
if (error) reject(error);
else resolve(result);
}
);
uploadStream.end(pdfBuffer);
});
console.log('[Cloudinary] Upload réussi, conversion en cours...');
const baseUrl = uploadResult.secure_url;
const pages = uploadResult.pages || 1;
const pageImages: PageImage[] = [];
// 2. Pour chaque page, télécharger l'image depuis Cloudinary et la stocker sur S3
for (let pageNum = 1; pageNum <= pages; pageNum++) {
// URL Cloudinary de la page
const cloudinaryPageUrl = baseUrl.replace(
'/upload/',
`/upload/pg_${pageNum}/w_1400,q_90/`
);
console.log(`[Cloudinary] Téléchargement page ${pageNum}/${pages} depuis Cloudinary...`);
// Télécharger l'image depuis Cloudinary
const imageResponse = await fetch(cloudinaryPageUrl);
if (!imageResponse.ok) {
throw new Error(`Erreur téléchargement page ${pageNum}: ${imageResponse.statusText}`);
}
const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
// 3. Stocker l'image sur S3
const s3Key = `odentas-sign-images/${requestId}/page-${pageNum}.jpg`;
await s3Client.send(
new PutObjectCommand({
Bucket: S3_BUCKET,
Key: s3Key,
Body: imageBuffer,
ContentType: 'image/jpeg',
CacheControl: 'public, max-age=31536000', // Cache 1 an
})
);
// Générer une URL presignée S3 (valide 24h, renouvelée à chaque accès)
const command = new GetObjectCommand({
Bucket: S3_BUCKET,
Key: s3Key,
});
const s3Url = await getSignedUrl(s3Client, command, { expiresIn: 86400 }); // 24h
pageImages.push({
pageNumber: pageNum,
imageUrl: s3Url,
width: 1400,
height: Math.round(1400 * 1.414), // Ratio A4
});
console.log(`[Cloudinary → S3] Page ${pageNum}/${pages} stockée sur S3: ${s3Key}`);
}
// 4. Nettoyer Cloudinary (supprimer le PDF temporaire)
try {
await cloudinary.uploader.destroy(`odentas-sign-temp/${requestId}/pdf-${requestId}`);
console.log('[Cloudinary] Nettoyage temporaire effectué');
} catch (cleanupError) {
console.warn('[Cloudinary] Erreur nettoyage (non bloquant):', cleanupError);
}
console.log(`[Cloudinary → S3] ✅ ${pages} page(s) converties et stockées sur S3`);
return pageImages;
} catch (error) {
console.error('[Cloudinary] Erreur:', error);
throw new Error(
`Erreur Cloudinary: ${error instanceof Error ? error.message : 'Erreur inconnue'}`
);
}
}
/**
* Supprime un PDF et ses pages de Cloudinary
*/
export async function deleteFromCloudinary(requestId: string): Promise<void> {
try {
await cloudinary.api.delete_resources_by_prefix(`odentas-sign/${requestId}`);
console.log(`[Cloudinary] PDF ${requestId} supprimé`);
} catch (error) {
console.error('[Cloudinary] Erreur suppression:', error);
}
}