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:
parent
69cb2c5a0a
commit
26132f38ce
1 changed files with 331 additions and 0 deletions
331
scripts/migrate-avenants-pdf-from-airtable.ts
Normal file
331
scripts/migrate-avenants-pdf-from-airtable.ts
Normal 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);
|
||||
Loading…
Reference in a new issue