- 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)
170 lines
5.1 KiB
JavaScript
170 lines
5.1 KiB
JavaScript
/**
|
|
* 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 attendu: source/{folder}/{requestId}-{timestamp}.pdf
|
|
const keyParts = key.split('/');
|
|
const filename = keyParts[keyParts.length - 1];
|
|
const requestId = filename.split('-')[0];
|
|
|
|
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;
|
|
}
|
|
};
|