Compare commits

...

4 Commits

5 changed files with 135 additions and 113 deletions

View File

@@ -21,19 +21,14 @@ RUN mkdir -p /app/config /app/data/pdfs
# Crée le fichier projects.json
COPY config/projects.json.example /app/config/projects.json
# Crée un utilisateur non-root pour la sécurité
RUN addgroup -g 1001 -S nodejs && \
adduser -S botuser -u 1001
# Change la propriété des fichiers
RUN chown -R botuser:nodejs /app
USER botuser
# Ajuste les permissions pour être plus permissives
RUN chmod -R 777 /app/config
# Définit les variables d'environnement par défaut
ENV NODE_ENV=production
ENV LOG_LEVEL=info
# Commande de démarrage
# Commande de démarrage (en tant que root)
CMD ["node", "src/index.js"]
# Labels pour la documentation

View File

@@ -21,7 +21,7 @@ services:
# Volumes pour la configuration
volumes:
- ./config/projects.json:/app/config/projects.json:ro # Fichier projects.json en lecture seule
- ./config/projects.json:/app/config/projects.json
- pdf-data:/app/data/pdfs #
# Réseau

View File

@@ -1,12 +1,9 @@
const fs = require('fs');
const os = require('os');
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 pdfjsLib = require('pdfjs-dist/legacy/build/pdf.js');
const logger = require('../../utils/logger');
const PdfRenderer = require('../../services/PdfRenderer');
const PdfStorage = require('../../services/PdfStorage');
/**
* Handler pour la commande /pdf
@@ -14,10 +11,9 @@ const logger = require('../../utils/logger');
class PdfHandler {
constructor() {
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 }
pdfjsLib.GlobalWorkerOptions.workerSrc = require('pdfjs-dist/build/pdf.worker.min.js');
this.ensureStorageDir();
this.loadSessions();
}
@@ -30,6 +26,11 @@ class PdfHandler {
const urlOption = interaction.options.getString('url');
const isPublic = interaction.options.getBoolean('public') === true;
if (attachment && urlOption) {
await interaction.editReply({ content: '❌ Fournis soit un fichier PDF, soit une URL, mais pas les deux.' });
return;
}
const sourceUrl = attachment ? attachment.url : urlOption;
if (!attachment && !urlOption) {
await interaction.editReply({ content: '❌ Fournis un PDF (fichier) ou une URL https:// vers un PDF.' });
@@ -51,8 +52,8 @@ class PdfHandler {
return;
}
const tmpPath = await this.downloadPdf(sourceUrl);
const { pdf, totalPages } = await this.loadPdf(tmpPath);
const tmpPath = await this.storage.downloadPdf(sourceUrl);
const { pdf, totalPages } = await this.renderer.loadPdf(tmpPath);
if (totalPages > 100) {
await interaction.editReply({ content: '❌ PDF trop long (max 100 pages).' });
@@ -134,9 +135,9 @@ class PdfHandler {
if (session.imagePaths?.[clampedPage - 1] && fs.existsSync(session.imagePaths[clampedPage - 1])) {
pageBuffer = fs.readFileSync(session.imagePaths[clampedPage - 1]);
} else {
const { pdf } = await this.loadPdf(session.pdfPath);
const buffer = await this.renderPage(pdf, clampedPage);
const filePath = this.writeImageFile(buffer, clampedPage);
const { pdf } = await this.renderer.loadPdf(session.pdfPath);
const buffer = await this.renderer.renderPage(pdf, clampedPage);
const filePath = this.storage.writeImageFile(buffer, clampedPage);
session.imagePaths = session.imagePaths || [];
session.imagePaths[clampedPage - 1] = filePath;
this.saveSessions();
@@ -186,69 +187,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
*/
async renderAllPages(pdf, totalPages) {
const paths = [];
for (let i = 1; i <= totalPages; i++) {
const buffer = await this.renderPage(pdf, i);
const filePath = this.writeImageFile(buffer, i);
const buffer = await this.renderer.renderPage(pdf, i);
const filePath = this.storage.writeImageFile(buffer, i);
paths.push(filePath);
}
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
*/
@@ -256,6 +207,7 @@ class PdfHandler {
const file = new AttachmentBuilder(pageBuffer, { name: `page-${pageIndex}.png` });
const embed = new EmbedBuilder()
.setTitle(title || 'Visualisation PDF')
.setURL(downloadUrl || null)
.setDescription(`Page ${pageIndex} / ${totalPages}`)
.setImage(`attachment://page-${pageIndex}.png`)
.setColor(0x5865F2);
@@ -310,15 +262,8 @@ class PdfHandler {
/**
* Persistance des sessions pour survivre aux redémarrages
*/
ensureStorageDir() {
fs.mkdirSync(this.storageDir, { recursive: true });
}
loadSessions() {
try {
if (!fs.existsSync(this.sessionFile)) return;
const raw = fs.readFileSync(this.sessionFile, 'utf8');
const data = JSON.parse(raw);
const data = this.storage.loadSessions();
data.forEach(entry => {
if (entry.pdfPath && fs.existsSync(entry.pdfPath)) {
this.sessions.set(entry.messageId, {
@@ -327,25 +272,19 @@ class PdfHandler {
ownerId: entry.ownerId,
createdAt: entry.createdAt,
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() {
try {
const data = [];
for (const [messageId, session] of this.sessions.entries()) {
data.push({ messageId, ...session });
}
fs.writeFileSync(this.sessionFile, JSON.stringify(data, null, 2), 'utf8');
} catch (e) {
logger.error('Erreur lors de la sauvegarde des sessions PDF:', e);
}
this.storage.saveSessions(data);
}
/**
@@ -353,14 +292,10 @@ class PdfHandler {
*/
async deleteSession(messageId, session) {
try {
if (session.imagePaths) {
session.imagePaths.forEach(p => {
if (p && fs.existsSync(p)) fs.unlinkSync(p);
});
}
if (session.pdfPath && fs.existsSync(session.pdfPath)) {
fs.unlinkSync(session.pdfPath);
}
const filesToDelete = [];
if (session.imagePaths) filesToDelete.push(...session.imagePaths);
if (session.pdfPath) filesToDelete.push(session.pdfPath);
this.storage.deleteFiles(filesToDelete);
this.sessions.delete(messageId);
this.saveSessions();
} catch (e) {

View 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;

View 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;