- 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)
218 lines
6.7 KiB
TypeScript
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);
|
|
}
|
|
}
|