// ======================================== // DIGITAL OCEAN TEMPLATES - RÉCUPÉRATION XML // Responsabilité: Récupération et cache des templates XML depuis DigitalOcean Spaces // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const fs = require('fs').promises; const path = require('path'); const axios = require('axios'); /** * DIGITAL OCEAN TEMPLATES MANAGER * Gestion récupération, cache et fallback des templates XML */ class DigitalOceanTemplates { constructor() { this.cacheDir = path.join(__dirname, '../../cache/templates'); this.config = { endpoint: process.env.DO_SPACES_ENDPOINT || 'https://fra1.digitaloceanspaces.com', bucket: process.env.DO_SPACES_BUCKET || 'autocollant', region: process.env.DO_SPACES_REGION || 'fra1', accessKey: process.env.DO_SPACES_KEY, secretKey: process.env.DO_SPACES_SECRET, timeout: 10000 // 10 secondes }; // Cache en mémoire this.memoryCache = new Map(); this.cacheExpiry = 5 * 60 * 1000; // 5 minutes // Templates par défaut this.defaultTemplates = { 'default.xml': this.getDefaultTemplate(), 'simple.xml': this.getSimpleTemplate(), 'advanced.xml': this.getAdvancedTemplate() }; this.initializeTemplateManager(); } /** * Initialise le gestionnaire de templates */ async initializeTemplateManager() { try { // Créer le dossier cache await fs.mkdir(this.cacheDir, { recursive: true }); // Vérifier la configuration DO this.checkConfiguration(); logSh('🌊 DigitalOceanTemplates initialisé', 'DEBUG'); } catch (error) { logSh(`❌ Erreur initialisation DigitalOceanTemplates: ${error.message}`, 'ERROR'); } } /** * Vérifie la configuration Digital Ocean */ checkConfiguration() { const hasCredentials = this.config.accessKey && this.config.secretKey; if (!hasCredentials) { logSh('⚠️ Credentials Digital Ocean manquantes, utilisation cache/fallback uniquement', 'WARNING'); } else { logSh('✅ Configuration Digital Ocean OK', 'DEBUG'); } return hasCredentials; } // ======================================== // RÉCUPÉRATION TEMPLATES // ======================================== /** * Récupère un template XML (avec cache et fallback) */ async getTemplate(filename) { return tracer.run('DigitalOceanTemplates.getTemplate', { filename }, async () => { if (!filename) { throw new Error('Nom de fichier template requis'); } logSh(`📋 Récupération template: ${filename}`, 'DEBUG'); try { // 1. Vérifier le cache mémoire const memoryCached = this.getFromMemoryCache(filename); if (memoryCached) { logSh(`⚡ Template ${filename} trouvé en cache mémoire`, 'DEBUG'); return memoryCached; } // 2. Vérifier le cache fichier const fileCached = await this.getFromFileCache(filename); if (fileCached) { logSh(`💾 Template ${filename} trouvé en cache fichier`, 'DEBUG'); this.setMemoryCache(filename, fileCached); return fileCached; } // 3. Récupérer depuis Digital Ocean if (this.checkConfiguration()) { try { const template = await this.fetchFromDigitalOcean(filename); if (template) { logSh(`🌊 Template ${filename} récupéré depuis Digital Ocean`, 'INFO'); // Sauvegarder en cache await this.saveToFileCache(filename, template); this.setMemoryCache(filename, template); return template; } } catch (doError) { logSh(`⚠️ Erreur Digital Ocean pour ${filename}: ${doError.message}`, 'WARNING'); } } // 4. Fallback sur template par défaut const defaultTemplate = this.getDefaultTemplateForFile(filename); logSh(`🔄 Utilisation template par défaut pour ${filename}`, 'WARNING'); return defaultTemplate; } catch (error) { logSh(`❌ Erreur récupération template ${filename}: ${error.message}`, 'ERROR'); // Fallback ultime return this.getDefaultTemplate(); } }); } /** * Récupère depuis Digital Ocean Spaces */ async fetchFromDigitalOcean(filename) { return tracer.run('DigitalOceanTemplates.fetchFromDigitalOcean', { filename }, async () => { const url = `${this.config.endpoint}/${this.config.bucket}/templates/${filename}`; logSh(`🌊 Récupération DO: ${url}`, 'DEBUG'); try { // Utiliser une requête simple sans authentification S3 complexe // Digital Ocean Spaces peut être configuré pour accès public aux templates const response = await axios.get(url, { timeout: this.config.timeout, responseType: 'text', headers: { 'Accept': 'application/xml, text/xml, text/plain' } }); if (response.status === 200 && response.data) { logSh(`✅ Template ${filename} récupéré (${response.data.length} chars)`, 'DEBUG'); return response.data; } throw new Error(`Réponse invalide: ${response.status}`); } catch (error) { if (error.response) { logSh(`❌ Digital Ocean error ${error.response.status}: ${error.response.statusText}`, 'WARNING'); } else { logSh(`❌ Digital Ocean network error: ${error.message}`, 'WARNING'); } throw error; } }); } // ======================================== // GESTION CACHE // ======================================== /** * Récupère depuis le cache mémoire */ getFromMemoryCache(filename) { const cached = this.memoryCache.get(filename); if (cached && Date.now() - cached.timestamp < this.cacheExpiry) { return cached.content; } if (cached) { this.memoryCache.delete(filename); } return null; } /** * Sauvegarde en cache mémoire */ setMemoryCache(filename, content) { this.memoryCache.set(filename, { content, timestamp: Date.now() }); } /** * Récupère depuis le cache fichier */ async getFromFileCache(filename) { try { const cachePath = path.join(this.cacheDir, filename); const stats = await fs.stat(cachePath); // Cache valide pendant 1 heure const maxAge = 60 * 60 * 1000; if (Date.now() - stats.mtime.getTime() < maxAge) { const content = await fs.readFile(cachePath, 'utf8'); return content; } } catch (error) { // Fichier cache n'existe pas ou erreur } return null; } /** * Sauvegarde en cache fichier */ async saveToFileCache(filename, content) { try { const cachePath = path.join(this.cacheDir, filename); await fs.writeFile(cachePath, content, 'utf8'); logSh(`💾 Template ${filename} sauvé en cache`, 'DEBUG'); } catch (error) { logSh(`⚠️ Erreur sauvegarde cache ${filename}: ${error.message}`, 'WARNING'); } } // ======================================== // TEMPLATES PAR DÉFAUT // ======================================== /** * Retourne le template par défaut approprié */ getDefaultTemplateForFile(filename) { const lowerFilename = filename.toLowerCase(); if (lowerFilename.includes('simple')) { return this.defaultTemplates['simple.xml']; } else if (lowerFilename.includes('advanced') || lowerFilename.includes('complet')) { return this.defaultTemplates['advanced.xml']; } return this.defaultTemplates['default.xml']; } /** * Template par défaut standard */ getDefaultTemplate() { return `

|Titre_Principal{{T0}}{Rédige un titre H1 accrocheur de maximum 10 mots pour {{MC0}}. Style {{personality.style}}}|

|Introduction{{MC0}}{Rédige une introduction engageante de 2-3 phrases qui présente {{MC0}} et donne envie de lire la suite. Ton {{personality.style}}}|

|Titre_H2_1{{MC+1_1}}{Crée un titre H2 informatif sur {{MC+1_1}}. Style {{personality.style}}}|

|Paragraphe_1{{MC+1_1}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_1}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|

|Titre_H2_2{{MC+1_2}}{Crée un titre H2 informatif sur {{MC+1_2}}. Style {{personality.style}}}|

|Paragraphe_2{{MC+1_2}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_2}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|

|Conclusion{{MC0}}{Conclusion engageante de 2 phrases sur {{MC0}}. Appel à l'action subtil. Ton {{personality.style}}}|
`; } /** * Template simple */ getSimpleTemplate() { return `

|Titre_H1{{T0}}{Titre principal pour {{MC0}}}|

|Introduction{{MC0}}{Introduction pour {{MC0}}}| |Contenu_Principal{{MC0}}{Contenu principal sur {{MC0}}}| |Conclusion{{MC0}}{Conclusion sur {{MC0}}}|
`; } /** * Template avancé */ getAdvancedTemplate() { return `

|Titre_Principal{{T0}}{Rédige un titre H1 accrocheur de maximum 10 mots pour {{MC0}}. Style {{personality.style}}}|

|Introduction{{MC0}}{Rédige une introduction engageante de 2-3 phrases qui présente {{MC0}} et donne envie de lire la suite. Ton {{personality.style}}}|

|Titre_H2_1{{MC+1_1}}{Crée un titre H2 informatif sur {{MC+1_1}}. Style {{personality.style}}}|

|Paragraphe_1{{MC+1_1}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_1}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|

|Titre_H2_2{{MC+1_2}}{Crée un titre H2 informatif sur {{MC+1_2}}. Style {{personality.style}}}|

|Paragraphe_2{{MC+1_2}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_2}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|

|Titre_H2_3{{MC+1_3}}{Crée un titre H2 informatif sur {{MC+1_3}}. Style {{personality.style}}}|

|Paragraphe_3{{MC+1_3}}{Explique en 4-5 phrases les avantages de {{MC+1_3}} pour {{MC0}}. Ton {{personality.style}}}|

|FAQ_Titre{Titre de section FAQ accrocheur sur {{MC0}}}|

|Faq_q_1{{MC+1_1}}{Question fréquente sur {{MC+1_1}} et {{MC0}}}| |Faq_a_1{{MC+1_1}}{Réponse claire et précise. 2-3 phrases. Ton {{personality.style}}}| |Faq_q_2{{MC+1_2}}{Question pratique sur {{MC+1_2}} en lien avec {{MC0}}}| |Faq_a_2{{MC+1_2}}{Réponse détaillée et utile. 2-3 phrases explicatives. Ton {{personality.style}}}| |Faq_q_3{{MC+1_3}}{Question sur {{MC+1_3}} que se posent les clients}| |Faq_a_3{{MC+1_3}}{Réponse complète qui rassure et informe. 2-3 phrases. Ton {{personality.style}}}|
|Conclusion{{MC0}}{Conclusion engageante de 2 phrases sur {{MC0}}. Appel à l'action subtil. Ton {{personality.style}}}|
`; } // ======================================== // UTILITAIRES // ======================================== /** * Liste les templates disponibles */ async listAvailableTemplates() { const templates = []; // Templates par défaut Object.keys(this.defaultTemplates).forEach(name => { templates.push({ name, source: 'default', cached: true }); }); // Templates en cache try { const cacheFiles = await fs.readdir(this.cacheDir); cacheFiles.forEach(file => { if (file.endsWith('.xml')) { templates.push({ name: file, source: 'cache', cached: true }); } }); } catch (error) { // Dossier cache n'existe pas } return templates; } /** * Vide le cache */ async clearCache() { try { // Vider cache mémoire this.memoryCache.clear(); // Vider cache fichier const cacheFiles = await fs.readdir(this.cacheDir); for (const file of cacheFiles) { if (file.endsWith('.xml')) { await fs.unlink(path.join(this.cacheDir, file)); } } logSh('🗑️ Cache templates vidé', 'INFO'); } catch (error) { logSh(`❌ Erreur vidage cache: ${error.message}`, 'ERROR'); } } /** * Retourne les statistiques du cache */ getCacheStats() { return { memoryCache: { size: this.memoryCache.size, expiry: this.cacheExpiry }, config: { hasCredentials: this.checkConfiguration(), endpoint: this.config.endpoint, bucket: this.config.bucket, timeout: this.config.timeout }, defaultTemplates: Object.keys(this.defaultTemplates).length }; } } // ============= EXPORTS ============= module.exports = { DigitalOceanTemplates };