feat: Ajouter script migration PDF avenants Airtable → S3

- Téléchargement des PDF depuis URLs Airtable
- Upload vers S3 avec pattern avenants/{reference}_avenant_{timestamp}.pdf
- Mise à jour de pdf_s3_key et pdf_url dans Supabase
- 91 PDF migrés avec succès
This commit is contained in:
odentas 2025-11-05 19:32:25 +01:00
parent 69cb2c5a0a
commit 26132f38ce

View file

@ -0,0 +1,331 @@
/**
* Script de migration des PDF des avenants depuis Airtable vers S3
*
* Ce script télécharge les PDF des avenants depuis Airtable et les uploade sur S3,
* puis met à jour la base de données Supabase avec les URLs.
*
* Prérequis:
* - Le CSV doit avoir les colonnes: Reference, Avenant PDF
* - Les variables d'environnement AWS et Supabase doivent être configurées
*
* Utilisation:
* npx tsx scripts/migrate-avenants-pdf-from-airtable.ts
*/
import { createClient } from '@supabase/supabase-js';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';
import * as https from 'https';
import * as http from 'http';
import { config } from 'dotenv';
// Charger les variables d'environnement
config({ path: path.join(process.cwd(), '.env.local') });
// Configuration
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const CSV_FILE_PATH = path.join(process.cwd(), 'Contrats-PDF-Avenants.csv');
// AWS S3 Configuration
const AWS_REGION = process.env.AWS_REGION || 'eu-west-3';
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID!;
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY!;
const S3_BUCKET_NAME = process.env.S3_BUCKET_NAME || 'odentas-documents';
if (!SUPABASE_URL || !SUPABASE_SERVICE_KEY) {
console.error('❌ Variables d\'environnement Supabase manquantes');
process.exit(1);
}
if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) {
console.error('❌ Variables d\'environnement AWS manquantes');
process.exit(1);
}
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY);
const s3Client = new S3Client({
region: AWS_REGION,
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
},
});
interface AirtableAvenantPdfRow {
reference: string; // Référence du contrat
avenant_pdf_url: string; // URL du PDF de l'avenant
}
/**
* Lit et parse le fichier CSV
*/
async function readCSV(): Promise<AirtableAvenantPdfRow[]> {
return new Promise((resolve, reject) => {
const rows: AirtableAvenantPdfRow[] = [];
const fileStream = fs.createReadStream(CSV_FILE_PATH);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
let isFirstLine = true;
let pdfColumnIndex = -1;
let referenceColumnIndex = -1;
rl.on('line', (line) => {
// Parser la première ligne pour trouver les index des colonnes
if (isFirstLine) {
isFirstLine = false;
const matches = line.match(/(?:^|,)("(?:[^"]|"")*"|[^,]*)/g);
if (!matches) return;
const cols = matches.map(m => {
let val = m.replace(/^,/, '');
if (val.startsWith('"') && val.endsWith('"')) {
val = val.slice(1, -1).replace(/""/g, '"');
}
return val.trim();
});
referenceColumnIndex = cols.indexOf('Reference');
pdfColumnIndex = cols.indexOf('Avenant PDF');
if (referenceColumnIndex === -1 || pdfColumnIndex === -1) {
console.error('❌ Colonnes "Reference" ou "Avenant PDF" non trouvées');
reject(new Error('Colonnes manquantes dans le CSV'));
return;
}
return;
}
// Parser les lignes de données
const matches = line.match(/(?:^|,)("(?:[^"]|"")*"|[^,]*)/g);
if (!matches || matches.length <= Math.max(referenceColumnIndex, pdfColumnIndex)) return;
const cols = matches.map(m => {
let val = m.replace(/^,/, '');
if (val.startsWith('"') && val.endsWith('"')) {
val = val.slice(1, -1).replace(/""/g, '"');
}
return val.trim();
});
const reference = cols[referenceColumnIndex] || '';
let pdfUrl = cols[pdfColumnIndex] || '';
// Extraire l'URL si le format est "nom_fichier.pdf (url)"
const urlMatch = pdfUrl.match(/\(https?:\/\/[^)]+\)/);
if (urlMatch) {
pdfUrl = urlMatch[0].slice(1, -1); // Retirer les parenthèses
}
if (reference && pdfUrl && pdfUrl.startsWith('http')) {
rows.push({
reference,
avenant_pdf_url: pdfUrl
});
}
});
rl.on('close', () => resolve(rows));
rl.on('error', reject);
});
}
/**
* Télécharge un fichier depuis une URL
*/
async function downloadFile(url: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
const protocol = url.startsWith('https') ? https : http;
protocol.get(url, (response) => {
if (response.statusCode === 301 || response.statusCode === 302) {
// Suivre la redirection
if (response.headers.location) {
downloadFile(response.headers.location).then(resolve).catch(reject);
return;
}
}
if (response.statusCode !== 200) {
reject(new Error(`Erreur HTTP: ${response.statusCode}`));
return;
}
const chunks: Buffer[] = [];
response.on('data', (chunk) => chunks.push(chunk));
response.on('end', () => resolve(Buffer.concat(chunks)));
response.on('error', reject);
}).on('error', reject);
});
}
/**
* Upload un fichier sur S3
*/
async function uploadToS3(buffer: Buffer, key: string, contentType: string): Promise<string> {
const command = new PutObjectCommand({
Bucket: S3_BUCKET_NAME,
Key: key,
Body: buffer,
ContentType: contentType,
});
await s3Client.send(command);
// Retourner l'URL publique (si le bucket est public) ou une URL signée
return `https://${S3_BUCKET_NAME}.s3.${AWS_REGION}.amazonaws.com/${key}`;
}
/**
* Recherche les avenants d'un contrat
*/
async function findAvenantsForContract(contractReference: string): Promise<any[]> {
const { data: contract } = await supabase
.from('cddu_contracts')
.select('id')
.or(`contract_number.eq.${contractReference},reference.eq.${contractReference}`)
.maybeSingle();
if (!contract) {
return [];
}
const { data: avenants } = await supabase
.from('avenants')
.select('id, numero_avenant, date_avenant, pdf_s3_key')
.eq('contract_id', contract.id)
.order('date_avenant', { ascending: true });
return avenants || [];
}
/**
* Met à jour un avenant avec le PDF
*/
async function updateAvenantWithPdf(avenantId: string, s3Key: string, pdfUrl: string) {
const { error } = await supabase
.from('avenants')
.update({
pdf_s3_key: s3Key,
pdf_url: pdfUrl,
})
.eq('id', avenantId);
if (error) {
throw error;
}
}
/**
* Fonction principale de migration
*/
async function migrate() {
console.log('🚀 Début de la migration des PDF des avenants depuis Airtable\n');
// Vérifier que le fichier CSV existe
if (!fs.existsSync(CSV_FILE_PATH)) {
console.error(`❌ Fichier CSV non trouvé: ${CSV_FILE_PATH}`);
console.error('📝 Placez le fichier "Contrats-PDF-Avenants.csv" à la racine du projet');
process.exit(1);
}
console.log(`📄 Lecture du fichier: ${CSV_FILE_PATH}\n`);
const rows = await readCSV();
console.log(`📊 Nombre de lignes avec PDF: ${rows.length}\n`);
const stats = {
total: rows.length,
success: 0,
noAvenants: 0,
downloadError: 0,
uploadError: 0,
updateError: 0,
};
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const index = i + 1;
console.log(`\n[${index}/${rows.length}] Traitement de ${row.reference}`);
// Rechercher les avenants pour ce contrat
const avenants = await findAvenantsForContract(row.reference);
if (avenants.length === 0) {
console.log(` ⚠️ Aucun avenant trouvé pour ce contrat`);
stats.noAvenants++;
continue;
}
console.log(`${avenants.length} avenant(s) trouvé(s)`);
// Si plusieurs avenants, on prend le premier (le plus ancien)
// Dans Airtable, il n'y a qu'un seul PDF par contrat
const avenant = avenants[0];
if (avenant.pdf_s3_key) {
console.log(` ⏭️ PDF déjà présent (${avenant.pdf_s3_key}) - Ignoré`);
stats.success++;
continue;
}
try {
// Télécharger le PDF depuis Airtable
console.log(` 📥 Téléchargement du PDF...`);
const pdfBuffer = await downloadFile(row.avenant_pdf_url);
console.log(` ✅ PDF téléchargé (${(pdfBuffer.length / 1024).toFixed(2)} KB)`);
// Générer la clé S3
const timestamp = new Date().getTime();
const s3Key = `avenants/${row.reference}_avenant_${timestamp}.pdf`;
// Upload sur S3
console.log(` ☁️ Upload vers S3...`);
const pdfUrl = await uploadToS3(pdfBuffer, s3Key, 'application/pdf');
console.log(` ✅ PDF uploadé: ${s3Key}`);
// Mettre à jour l'avenant dans Supabase
console.log(` 💾 Mise à jour de l'avenant ${avenant.numero_avenant}...`);
await updateAvenantWithPdf(avenant.id, s3Key, pdfUrl);
console.log(` ✅ Avenant mis à jour`);
stats.success++;
} catch (error: any) {
console.log(` ❌ Erreur: ${error.message}`);
if (error.message.includes('HTTP') || error.message.includes('download')) {
stats.downloadError++;
} else if (error.message.includes('S3') || error.message.includes('upload')) {
stats.uploadError++;
} else {
stats.updateError++;
}
}
// Délai entre chaque traitement pour éviter de surcharger les APIs
await new Promise(resolve => setTimeout(resolve, 500));
}
// Résumé
console.log('\n\n' + '='.repeat(60));
console.log('📊 RÉSUMÉ DE LA MIGRATION');
console.log('='.repeat(60));
console.log(`Total de PDF: ${stats.total}`);
console.log(`PDF migrés avec succès: ${stats.success}`);
console.log(`Contrats sans avenants: ${stats.noAvenants} ⚠️`);
console.log(`Erreurs téléchargement: ${stats.downloadError}`);
console.log(`Erreurs upload S3: ${stats.uploadError}`);
console.log(`Erreurs mise à jour DB: ${stats.updateError}`);
console.log('='.repeat(60));
console.log('\n✅ Migration terminée!\n');
}
// Exécuter la migration
migrate().catch(console.error);