refactor: extract PDF rendering and storage logic into dedicated service classes
This commit is contained in:
@@ -1,12 +1,9 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const axios = require('axios');
|
|
||||||
const sharp = require('sharp');
|
|
||||||
const { createCanvas } = require('canvas');
|
|
||||||
const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder } = require('discord.js');
|
const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder } = require('discord.js');
|
||||||
const pdfjsLib = require('pdfjs-dist/legacy/build/pdf.js');
|
|
||||||
const logger = require('../../utils/logger');
|
const logger = require('../../utils/logger');
|
||||||
|
const PdfRenderer = require('../../services/PdfRenderer');
|
||||||
|
const PdfStorage = require('../../services/PdfStorage');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler pour la commande /pdf
|
* Handler pour la commande /pdf
|
||||||
@@ -14,10 +11,9 @@ const logger = require('../../utils/logger');
|
|||||||
class PdfHandler {
|
class PdfHandler {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.storageDir = process.env.PDF_STORAGE_DIR || path.join(process.cwd(), 'data', 'pdfs');
|
this.storageDir = process.env.PDF_STORAGE_DIR || path.join(process.cwd(), 'data', 'pdfs');
|
||||||
this.sessionFile = path.join(this.storageDir, 'sessions.json');
|
this.storage = new PdfStorage(this.storageDir);
|
||||||
|
this.renderer = new PdfRenderer();
|
||||||
this.sessions = new Map(); // messageId -> { pdfPath, totalPages, ownerId, createdAt, downloadUrl, isPublic, imagePaths }
|
this.sessions = new Map(); // messageId -> { pdfPath, totalPages, ownerId, createdAt, downloadUrl, isPublic, imagePaths }
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = require('pdfjs-dist/build/pdf.worker.min.js');
|
|
||||||
this.ensureStorageDir();
|
|
||||||
this.loadSessions();
|
this.loadSessions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,8 +47,8 @@ class PdfHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tmpPath = await this.downloadPdf(sourceUrl);
|
const tmpPath = await this.storage.downloadPdf(sourceUrl);
|
||||||
const { pdf, totalPages } = await this.loadPdf(tmpPath);
|
const { pdf, totalPages } = await this.renderer.loadPdf(tmpPath);
|
||||||
|
|
||||||
if (totalPages > 100) {
|
if (totalPages > 100) {
|
||||||
await interaction.editReply({ content: '❌ PDF trop long (max 100 pages).' });
|
await interaction.editReply({ content: '❌ PDF trop long (max 100 pages).' });
|
||||||
@@ -134,9 +130,9 @@ class PdfHandler {
|
|||||||
if (session.imagePaths?.[clampedPage - 1] && fs.existsSync(session.imagePaths[clampedPage - 1])) {
|
if (session.imagePaths?.[clampedPage - 1] && fs.existsSync(session.imagePaths[clampedPage - 1])) {
|
||||||
pageBuffer = fs.readFileSync(session.imagePaths[clampedPage - 1]);
|
pageBuffer = fs.readFileSync(session.imagePaths[clampedPage - 1]);
|
||||||
} else {
|
} else {
|
||||||
const { pdf } = await this.loadPdf(session.pdfPath);
|
const { pdf } = await this.renderer.loadPdf(session.pdfPath);
|
||||||
const buffer = await this.renderPage(pdf, clampedPage);
|
const buffer = await this.renderer.renderPage(pdf, clampedPage);
|
||||||
const filePath = this.writeImageFile(buffer, clampedPage);
|
const filePath = this.storage.writeImageFile(buffer, clampedPage);
|
||||||
session.imagePaths = session.imagePaths || [];
|
session.imagePaths = session.imagePaths || [];
|
||||||
session.imagePaths[clampedPage - 1] = filePath;
|
session.imagePaths[clampedPage - 1] = filePath;
|
||||||
this.saveSessions();
|
this.saveSessions();
|
||||||
@@ -186,69 +182,19 @@ class PdfHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Télécharge le PDF dans un fichier temporaire
|
|
||||||
*/
|
|
||||||
async downloadPdf(url) {
|
|
||||||
const response = await axios.get(url, { responseType: 'arraybuffer' });
|
|
||||||
const fileName = `pdf-${Date.now()}-${Math.random().toString(36).slice(2)}.pdf`;
|
|
||||||
const targetPath = path.join(this.storageDir, fileName);
|
|
||||||
fs.writeFileSync(targetPath, response.data);
|
|
||||||
return targetPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Charge le PDF avec pdfjs
|
|
||||||
*/
|
|
||||||
async loadPdf(pdfPath) {
|
|
||||||
const data = new Uint8Array(fs.readFileSync(pdfPath));
|
|
||||||
const loadingTask = pdfjsLib.getDocument({ data });
|
|
||||||
const pdf = await loadingTask.promise;
|
|
||||||
return { pdf, totalPages: pdf.numPages };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rendu d'une page en buffer PNG
|
|
||||||
*/
|
|
||||||
async renderPage(pdf, pageNumber) {
|
|
||||||
const page = await pdf.getPage(pageNumber);
|
|
||||||
const viewport = page.getViewport({ scale: 1.5 }); // Ajuster si besoin
|
|
||||||
const canvas = createCanvas(viewport.width, viewport.height);
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
|
|
||||||
await page.render({
|
|
||||||
canvasContext: context,
|
|
||||||
viewport,
|
|
||||||
}).promise;
|
|
||||||
|
|
||||||
const buffer = canvas.toBuffer('image/png');
|
|
||||||
// Recompression légère pour réduire la taille
|
|
||||||
return sharp(buffer).png({ quality: 90 }).toBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rend toutes les pages et retourne la liste des chemins de fichiers
|
* Rend toutes les pages et retourne la liste des chemins de fichiers
|
||||||
*/
|
*/
|
||||||
async renderAllPages(pdf, totalPages) {
|
async renderAllPages(pdf, totalPages) {
|
||||||
const paths = [];
|
const paths = [];
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
const buffer = await this.renderPage(pdf, i);
|
const buffer = await this.renderer.renderPage(pdf, i);
|
||||||
const filePath = this.writeImageFile(buffer, i);
|
const filePath = this.storage.writeImageFile(buffer, i);
|
||||||
paths.push(filePath);
|
paths.push(filePath);
|
||||||
}
|
}
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Écrit une image en cache et retourne le chemin
|
|
||||||
*/
|
|
||||||
writeImageFile(buffer, pageIndex) {
|
|
||||||
const fileName = `page-${pageIndex}-${Date.now()}-${Math.random().toString(36).slice(2)}.png`;
|
|
||||||
const targetPath = path.join(this.storageDir, fileName);
|
|
||||||
fs.writeFileSync(targetPath, buffer);
|
|
||||||
return targetPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construit le message Discord avec image + boutons
|
* Construit le message Discord avec image + boutons
|
||||||
*/
|
*/
|
||||||
@@ -311,15 +257,8 @@ class PdfHandler {
|
|||||||
/**
|
/**
|
||||||
* Persistance des sessions pour survivre aux redémarrages
|
* Persistance des sessions pour survivre aux redémarrages
|
||||||
*/
|
*/
|
||||||
ensureStorageDir() {
|
|
||||||
fs.mkdirSync(this.storageDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSessions() {
|
loadSessions() {
|
||||||
try {
|
const data = this.storage.loadSessions();
|
||||||
if (!fs.existsSync(this.sessionFile)) return;
|
|
||||||
const raw = fs.readFileSync(this.sessionFile, 'utf8');
|
|
||||||
const data = JSON.parse(raw);
|
|
||||||
data.forEach(entry => {
|
data.forEach(entry => {
|
||||||
if (entry.pdfPath && fs.existsSync(entry.pdfPath)) {
|
if (entry.pdfPath && fs.existsSync(entry.pdfPath)) {
|
||||||
this.sessions.set(entry.messageId, {
|
this.sessions.set(entry.messageId, {
|
||||||
@@ -328,25 +267,19 @@ class PdfHandler {
|
|||||||
ownerId: entry.ownerId,
|
ownerId: entry.ownerId,
|
||||||
createdAt: entry.createdAt,
|
createdAt: entry.createdAt,
|
||||||
downloadUrl: entry.downloadUrl,
|
downloadUrl: entry.downloadUrl,
|
||||||
isPublic: entry.isPublic
|
isPublic: entry.isPublic,
|
||||||
|
imagePaths: entry.imagePaths
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
logger.error('Erreur lors du chargement des sessions PDF persistées:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSessions() {
|
saveSessions() {
|
||||||
try {
|
|
||||||
const data = [];
|
const data = [];
|
||||||
for (const [messageId, session] of this.sessions.entries()) {
|
for (const [messageId, session] of this.sessions.entries()) {
|
||||||
data.push({ messageId, ...session });
|
data.push({ messageId, ...session });
|
||||||
}
|
}
|
||||||
fs.writeFileSync(this.sessionFile, JSON.stringify(data, null, 2), 'utf8');
|
this.storage.saveSessions(data);
|
||||||
} catch (e) {
|
|
||||||
logger.error('Erreur lors de la sauvegarde des sessions PDF:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -354,14 +287,10 @@ class PdfHandler {
|
|||||||
*/
|
*/
|
||||||
async deleteSession(messageId, session) {
|
async deleteSession(messageId, session) {
|
||||||
try {
|
try {
|
||||||
if (session.imagePaths) {
|
const filesToDelete = [];
|
||||||
session.imagePaths.forEach(p => {
|
if (session.imagePaths) filesToDelete.push(...session.imagePaths);
|
||||||
if (p && fs.existsSync(p)) fs.unlinkSync(p);
|
if (session.pdfPath) filesToDelete.push(session.pdfPath);
|
||||||
});
|
this.storage.deleteFiles(filesToDelete);
|
||||||
}
|
|
||||||
if (session.pdfPath && fs.existsSync(session.pdfPath)) {
|
|
||||||
fs.unlinkSync(session.pdfPath);
|
|
||||||
}
|
|
||||||
this.sessions.delete(messageId);
|
this.sessions.delete(messageId);
|
||||||
this.saveSessions();
|
this.saveSessions();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
32
src/services/PdfRenderer.js
Normal file
32
src/services/PdfRenderer.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const { createCanvas } = require('canvas');
|
||||||
|
const sharp = require('sharp');
|
||||||
|
const pdfjsLib = require('pdfjs-dist/legacy/build/pdf.js');
|
||||||
|
|
||||||
|
// Configure le worker pdfjs
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = require('pdfjs-dist/build/pdf.worker.min.js');
|
||||||
|
|
||||||
|
class PdfRenderer {
|
||||||
|
async loadPdf(pdfPath) {
|
||||||
|
const data = new Uint8Array(require('fs').readFileSync(pdfPath));
|
||||||
|
const loadingTask = pdfjsLib.getDocument({ data });
|
||||||
|
const pdf = await loadingTask.promise;
|
||||||
|
return { pdf, totalPages: pdf.numPages };
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderPage(pdf, pageNumber) {
|
||||||
|
const page = await pdf.getPage(pageNumber);
|
||||||
|
const viewport = page.getViewport({ scale: 1.5 });
|
||||||
|
const canvas = createCanvas(viewport.width, viewport.height);
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
|
await page.render({
|
||||||
|
canvasContext: context,
|
||||||
|
viewport,
|
||||||
|
}).promise;
|
||||||
|
|
||||||
|
const buffer = canvas.toBuffer('image/png');
|
||||||
|
return sharp(buffer).png({ quality: 90 }).toBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PdfRenderer;
|
||||||
60
src/services/PdfStorage.js
Normal file
60
src/services/PdfStorage.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const axios = require('axios');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
|
class PdfStorage {
|
||||||
|
constructor(baseDir) {
|
||||||
|
this.baseDir = baseDir || path.join(process.cwd(), 'data', 'pdfs');
|
||||||
|
this.sessionFile = path.join(this.baseDir, 'sessions.json');
|
||||||
|
this.ensureDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureDir() {
|
||||||
|
fs.mkdirSync(this.baseDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadPdf(url) {
|
||||||
|
const response = await axios.get(url, { responseType: 'arraybuffer' });
|
||||||
|
const fileName = `pdf-${Date.now()}-${Math.random().toString(36).slice(2)}.pdf`;
|
||||||
|
const targetPath = path.join(this.baseDir, fileName);
|
||||||
|
fs.writeFileSync(targetPath, response.data);
|
||||||
|
return targetPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeImageFile(buffer, pageIndex) {
|
||||||
|
const fileName = `page-${pageIndex}-${Date.now()}-${Math.random().toString(36).slice(2)}.png`;
|
||||||
|
const targetPath = path.join(this.baseDir, fileName);
|
||||||
|
fs.writeFileSync(targetPath, buffer);
|
||||||
|
return targetPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSessions() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(this.sessionFile)) return [];
|
||||||
|
const raw = fs.readFileSync(this.sessionFile, 'utf8');
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Erreur lors du chargement des sessions PDF persistées:', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSessions(data) {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(this.sessionFile, JSON.stringify(data, null, 2), 'utf8');
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Erreur lors de la sauvegarde des sessions PDF:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFiles(paths) {
|
||||||
|
paths.forEach(p => {
|
||||||
|
if (p && fs.existsSync(p)) {
|
||||||
|
try { fs.unlinkSync(p); } catch (e) { logger.warn('Erreur suppression fichier PDF/PNG', e); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PdfStorage;
|
||||||
Reference in New Issue
Block a user