espace-paie-odentas/lambda-pdf-converter/index.js

170 lines
5.2 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: 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;
}
};