/** * Lambda déclenchée par S3 ObjectCreated * Convertit automatiquement les PDF en images JPEG avec pdftoppm * et les stocke dans S3 pour les pages de signature */ const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3'); const { spawn } = require('child_process'); const fs = require('fs').promises; const path = require('path'); const { pipeline } = require('stream/promises'); // Client S3 const s3Client = new S3Client({ region: process.env.AWS_REGION || 'eu-west-3' }); const SOURCE_BUCKET = process.env.SOURCE_BUCKET || 'odentas-sign'; const DEST_BUCKET = process.env.DEST_BUCKET || 'odentas-docs'; /** * Stream un objet S3 vers un fichier local */ async function streamToFile(readable, filePath) { const fileHandle = await fs.open(filePath, 'w'); const writable = fileHandle.createWriteStream(); await pipeline(readable, writable); } /** * Convertit un PDF en images JPEG avec pdftoppm */ async function convertPdfToImages(pdfPath, requestId, outputDir) { const outputPrefix = path.join(outputDir, 'page'); const args = [ '-jpeg', '-jpegopt', 'quality=90', '-r', '150', // 150 DPI pour bonne qualité pdfPath, outputPrefix ]; return new Promise((resolve, reject) => { const proc = spawn('pdftoppm', args); let stderr = ''; proc.stderr.on('data', (data) => { stderr += data.toString(); }); proc.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`pdftoppm exit ${code}: ${stderr}`)); } }); }); } /** * Handler principal */ exports.handler = async (event) => { console.log('[Lambda] Event reçu:', JSON.stringify(event, null, 2)); try { // Traiter chaque record S3 for (const record of event.Records || []) { const bucket = record.s3.bucket.name; const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' ')); console.log(`[Lambda] Fichier détecté: s3://${bucket}/${key}`); if (!key.toLowerCase().endsWith('.pdf')) { console.log('[Lambda] Ignoré (pas un PDF)'); continue; } // Extraire le requestId depuis la clé S3 // Format: source/{folder}/TEST-1761729511580.pdf → requestId = TEST-1761729511580 const keyParts = key.split('/'); const filename = keyParts[keyParts.length - 1]; const requestId = filename.replace(/\.pdf$/i, ''); // Enlever l'extension if (!requestId) { throw new Error(`Impossible d'extraire requestId depuis la clé: ${key}`); } console.log(`[Lambda] Request ID extrait: ${requestId}`); const tmpDir = '/tmp'; const pdfPath = path.join(tmpDir, `${requestId}.pdf`); // 1. Télécharger le PDF depuis S3 console.log('[Lambda] Téléchargement du PDF depuis S3...'); const getResponse = await s3Client.send( new GetObjectCommand({ Bucket: bucket, Key: key, }) ); await streamToFile(getResponse.Body, pdfPath); const stats = await fs.stat(pdfPath); console.log(`[Lambda] PDF téléchargé: ${stats.size} bytes`); // 2. Convertir avec pdftoppm → génère /tmp/page-1.jpg, /tmp/page-2.jpg ... console.log('[Lambda] Conversion PDF → JPEG avec pdftoppm...'); await convertPdfToImages(pdfPath, requestId, tmpDir); // 3. Lister les images générées const files = await fs.readdir(tmpDir); const pageFiles = files .filter(f => f.startsWith('page-') && f.endsWith('.jpg')) .sort((a, b) => { const numA = parseInt(a.match(/page-(\d+)/)[1], 10); const numB = parseInt(b.match(/page-(\d+)/)[1], 10); return numA - numB; }); console.log(`[Lambda] ${pageFiles.length} page(s) générée(s):`, pageFiles); if (pageFiles.length === 0) { throw new Error('Aucune page générée par pdftoppm'); } // 4. Upload vers S3 const uploadedPages = []; for (let i = 0; i < pageFiles.length; i++) { const pageFile = pageFiles[i]; const pageNum = i + 1; const imagePath = path.join(tmpDir, pageFile); console.log(`[Lambda] Upload page ${pageNum}: ${pageFile}`); const imageBuffer = await fs.readFile(imagePath); const s3Key = `odentas-sign-images/${requestId}/page-${pageNum}.jpg`; await s3Client.send( new PutObjectCommand({ Bucket: DEST_BUCKET, Key: s3Key, Body: imageBuffer, ContentType: 'image/jpeg', CacheControl: 'public, max-age=31536000', // 1 an }) ); console.log(`[Lambda] Page ${pageNum} stockée sur S3 → ${s3Key}`); uploadedPages.push(s3Key); // Supprimer le fichier temporaire await fs.unlink(imagePath); } // 5. Cleanup du PDF await fs.unlink(pdfPath); console.log(`[Lambda] ✅ Conversion terminée: ${uploadedPages.length} page(s) pour ${requestId}`); } return { statusCode: 200, body: JSON.stringify({ message: 'Conversion réussie' }), }; } catch (error) { console.error('[Lambda] ❌ Erreur:', error); throw error; } };