import { Request, Response } from 'express'; import { PrismaClient } from '@prisma/client'; import { PDFService } from '../services/pdfService'; import multer from 'multer'; import path from 'path'; import fs from 'fs/promises'; import { v4 as uuidv4 } from 'uuid'; const prisma = new PrismaClient(); // Configuração do multer para upload de arquivos const storage = multer.diskStorage({ destination: async (req, file, cb) => { const uploadDir = path.join(process.cwd(), 'storage', 'temp', 'uploads'); await fs.mkdir(uploadDir, { recursive: true }); cb(null, uploadDir); }, filename: (req, file, cb) => { const uniqueName = `${uuidv4()}${path.extname(file.originalname)}`; cb(null, uniqueName); }, }); const upload = multer({ storage, limits: { fileSize: 100 * 1024 * 1024, // 100MB }, fileFilter: (req, file, cb) => { const allowedMimes = [ 'application/pdf', 'image/png', 'image/jpeg', 'image/jpg', 'image/webp', ]; if (allowedMimes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('Tipo de arquivo não permitido. Use PDF ou imagens (PNG, JPG, WEBP).')); } }, }); export const uploadMiddleware = upload; /** * Controller para operações de PDF */ export class PDFController { /** * Comprime um ou mais PDFs */ static async compress(req: Request, res: Response): Promise { const startTime = Date.now(); let operacaoId: string | null = null; try { const files = req.files as Express.Multer.File[]; const userId = (req as any).user?.id; if (!files || files.length === 0) { res.status(400).json({ error: 'Nenhum arquivo enviado' }); return; } if (!userId) { res.status(401).json({ error: 'Usuário não autenticado' }); return; } const quality = req.body.quality ? parseInt(req.body.quality) : 95; const maxSizeMB = req.body.maxSizeMB ? parseInt(req.body.maxSizeMB) : undefined; const arquivosOriginais: string[] = []; const arquivosResultado: string[] = []; let tamanhoOriginal = BigInt(0); let tamanhoResultado = BigInt(0); // Cria registro da operação no banco const operacao = await prisma.operacaoPDF.create({ data: { tipoOperacao: 'COMPRIMIR', status: 'PROCESSANDO', usuarioId: userId, arquivosOriginais: [], arquivosResultado: [], tamanhoOriginal: BigInt(0), parametros: { quality, maxSizeMB }, }, }); operacaoId = operacao.id; // Processa cada arquivo const results = []; for (const file of files) { arquivosOriginais.push(file.path); tamanhoOriginal += BigInt(file.size); const result = await PDFService.compressPDF(file.path, undefined, { quality, maxSizeMB, }); arquivosResultado.push(result.outputPath); tamanhoResultado += BigInt(result.compressedSize); results.push({ originalName: file.originalname, originalSize: result.originalSize, compressedSize: result.compressedSize, reduction: result.reduction, outputPath: result.outputPath, }); // Remove o arquivo original await fs.unlink(file.path).catch(() => {}); } const tempoProcessamento = Date.now() - startTime; // Atualiza a operação no banco await prisma.operacaoPDF.update({ where: { id: operacaoId }, data: { status: 'CONCLUIDO', arquivosOriginais, arquivosResultado, tamanhoOriginal, tamanhoResultado, tempoProcessamento, }, }); res.json({ success: true, operacaoId, results, totalOriginalSize: Number(tamanhoOriginal), totalCompressedSize: Number(tamanhoResultado), totalReduction: ((Number(tamanhoOriginal) - Number(tamanhoResultado)) / Number(tamanhoOriginal)) * 100, processingTime: tempoProcessamento, }); } catch (error) { console.error('Erro ao comprimir PDF:', error); if (operacaoId) { await prisma.operacaoPDF.update({ where: { id: operacaoId }, data: { status: 'ERRO', mensagemErro: error instanceof Error ? error.message : 'Erro desconhecido', tempoProcessamento: Date.now() - startTime, }, }); } res.status(500).json({ error: 'Falha ao comprimir PDF', message: error instanceof Error ? error.message : 'Erro desconhecido', }); } } /** * Divide um PDF */ static async split(req: Request, res: Response): Promise { const startTime = Date.now(); let operacaoId: string | null = null; try { const file = (req.files as Express.Multer.File[])?.[0]; const userId = (req as any).user?.id; if (!file) { res.status(400).json({ error: 'Nenhum arquivo enviado' }); return; } if (!userId) { res.status(401).json({ error: 'Usuário não autenticado' }); return; } const { splitType, pages, ranges, chunkSize } = req.body; if (!splitType) { res.status(400).json({ error: 'Tipo de divisão não especificado' }); return; } const options: any = { splitType }; if (splitType === 'pages' && pages) { options.pages = JSON.parse(pages); } else if (splitType === 'range' && ranges) { options.ranges = JSON.parse(ranges); } else if (splitType === 'chunks' && chunkSize) { options.chunkSize = parseInt(chunkSize); } // Cria registro da operação const operacao = await prisma.operacaoPDF.create({ data: { tipoOperacao: 'DIVIDIR', status: 'PROCESSANDO', usuarioId: userId, arquivosOriginais: [file.path], arquivosResultado: [], tamanhoOriginal: BigInt(file.size), parametros: options, }, }); operacaoId = operacao.id; // Divide o PDF const outputPaths = await PDFService.splitPDF(file.path, options); // Calcula tamanho total dos arquivos resultantes let tamanhoResultado = BigInt(0); for (const outputPath of outputPaths) { const stats = await fs.stat(outputPath); tamanhoResultado += BigInt(stats.size); } const tempoProcessamento = Date.now() - startTime; // Atualiza a operação await prisma.operacaoPDF.update({ where: { id: operacaoId }, data: { status: 'CONCLUIDO', arquivosResultado: outputPaths, tamanhoResultado, tempoProcessamento, }, }); // Remove o arquivo original await fs.unlink(file.path).catch(() => {}); res.json({ success: true, operacaoId, filesCreated: outputPaths.length, outputPaths, processingTime: tempoProcessamento, }); } catch (error) { console.error('Erro ao dividir PDF:', error); if (operacaoId) { await prisma.operacaoPDF.update({ where: { id: operacaoId }, data: { status: 'ERRO', mensagemErro: error instanceof Error ? error.message : 'Erro desconhecido', tempoProcessamento: Date.now() - startTime, }, }); } res.status(500).json({ error: 'Falha ao dividir PDF', message: error instanceof Error ? error.message : 'Erro desconhecido', }); } } /** * Mescla múltiplos PDFs */ static async merge(req: Request, res: Response): Promise { const startTime = Date.now(); let operacaoId: string | null = null; try { const files = req.files as Express.Multer.File[]; const userId = (req as any).user?.id; if (!files || files.length < 2) { res.status(400).json({ error: 'São necessários pelo menos 2 arquivos para mesclar' }); return; } if (!userId) { res.status(401).json({ error: 'Usuário não autenticado' }); return; } const filePaths = files.map((f) => f.path); let tamanhoOriginal = BigInt(0); for (const file of files) { tamanhoOriginal += BigInt(file.size); } // Cria registro da operação const operacao = await prisma.operacaoPDF.create({ data: { tipoOperacao: 'MESCLAR', status: 'PROCESSANDO', usuarioId: userId, arquivosOriginais: filePaths, arquivosResultado: [], tamanhoOriginal, parametros: { filesCount: files.length }, }, }); operacaoId = operacao.id; // Mescla os PDFs const outputPath = await PDFService.mergePDFs({ filePaths, outputPath: path.join(process.cwd(), 'storage', 'pdf-output', `merged-${uuidv4()}.pdf`), }); // Calcula tamanho do arquivo resultante const stats = await fs.stat(outputPath); const tamanhoResultado = BigInt(stats.size); const tempoProcessamento = Date.now() - startTime; // Atualiza a operação await prisma.operacaoPDF.update({ where: { id: operacaoId }, data: { status: 'CONCLUIDO', arquivosResultado: [outputPath], tamanhoResultado, tempoProcessamento, }, }); // Remove os arquivos originais for (const filePath of filePaths) { await fs.unlink(filePath).catch(() => {}); } res.json({ success: true, operacaoId, outputPath, originalSize: Number(tamanhoOriginal), mergedSize: Number(tamanhoResultado), processingTime: tempoProcessamento, }); } catch (error) { console.error('Erro ao mesclar PDFs:', error); if (operacaoId) { await prisma.operacaoPDF.update({ where: { id: operacaoId }, data: { status: 'ERRO', mensagemErro: error instanceof Error ? error.message : 'Erro desconhecido', tempoProcessamento: Date.now() - startTime, }, }); } res.status(500).json({ error: 'Falha ao mesclar PDFs', message: error instanceof Error ? error.message : 'Erro desconhecido', }); } } /** * Converte PDF em imagens */ static async pdfToImages(req: Request, res: Response): Promise { const startTime = Date.now(); let operacaoId: string | null = null; try { const file = (req.files as Express.Multer.File[])?.[0]; const userId = (req as any).user?.id; if (!file) { res.status(400).json({ error: 'Nenhum arquivo enviado' }); return; } if (!userId) { res.status(401).json({ error: 'Usuário não autenticado' }); return; } const format = req.body.format || 'png'; const quality = req.body.quality ? parseInt(req.body.quality) : 95; const density = req.body.density ? parseInt(req.body.density) : 300; const pages = req.body.pages ? JSON.parse(req.body.pages) : 'all'; // Cria registro da operação const operacao = await prisma.operacaoPDF.create({ data: { tipoOperacao: 'PDF_PARA_IMAGEM', status: 'PROCESSANDO', usuarioId: userId, arquivosOriginais: [file.path], arquivosResultado: [], tamanhoOriginal: BigInt(file.size), parametros: { format, quality, density, pages }, }, }); operacaoId = operacao.id; // Converte para imagens const outputPaths = await PDFService.pdfToImages(file.path, { format, quality, density, pages, }); // Calcula tamanho total let tamanhoResultado = BigInt(0); for (const outputPath of outputPaths) { const stats = await fs.stat(outputPath); tamanhoResultado += BigInt(stats.size); } const tempoProcessamento = Date.now() - startTime; // Atualiza a operação await prisma.operacaoPDF.update({ where: { id: operacaoId }, data: { status: 'CONCLUIDO', arquivosResultado: outputPaths, tamanhoResultado, tempoProcessamento, }, }); // Remove o arquivo original await fs.unlink(file.path).catch(() => {}); res.json({ success: true, operacaoId, imagesCreated: outputPaths.length, outputPaths, processingTime: tempoProcessamento, }); } catch (error) { console.error('Erro ao converter PDF para imagens:', error); if (operacaoId) { await prisma.operacaoPDF.update({ where: { id: operacaoId }, data: { status: 'ERRO', mensagemErro: error instanceof Error ? error.message : 'Erro desconhecido', tempoProcessamento: Date.now() - startTime, }, }); } res.status(500).json({ error: 'Falha ao converter PDF para imagens', message: error instanceof Error ? error.message : 'Erro desconhecido', }); } } /** * Converte imagens em PDF */ static async imagesToPDF(req: Request, res: Response): Promise { const startTime = Date.now(); let operacaoId: string | null = null; try { const files = req.files as Express.Multer.File[]; const userId = (req as any).user?.id; if (!files || files.length === 0) { res.status(400).json({ error: 'Nenhuma imagem enviada' }); return; } if (!userId) { res.status(401).json({ error: 'Usuário não autenticado' }); return; } const imagePaths = files.map((f) => f.path); let tamanhoOriginal = BigInt(0); for (const file of files) { tamanhoOriginal += BigInt(file.size); } const fitToPage = req.body.fitToPage === 'true'; const pageSize = req.body.pageSize ? JSON.parse(req.body.pageSize) : undefined; // Cria registro da operação const operacao = await prisma.operacaoPDF.create({ data: { tipoOperacao: 'IMAGEM_PARA_PDF', status: 'PROCESSANDO', usuarioId: userId, arquivosOriginais: imagePaths, arquivosResultado: [], tamanhoOriginal, parametros: { fitToPage, pageSize, imagesCount: files.length }, }, }); operacaoId = operacao.id; // Converte para PDF const outputPath = await PDFService.imagesToPDF({ imagePaths, outputPath: path.join(process.cwd(), 'storage', 'pdf-output', `images-to-pdf-${uuidv4()}.pdf`), fitToPage, pageSize, }); // Calcula tamanho do arquivo resultante const stats = await fs.stat(outputPath); const tamanhoResultado = BigInt(stats.size); const tempoProcessamento = Date.now() - startTime; // Atualiza a operação await prisma.operacaoPDF.update({ where: { id: operacaoId }, data: { status: 'CONCLUIDO', arquivosResultado: [outputPath], tamanhoResultado, tempoProcessamento, }, }); // Remove os arquivos originais for (const imagePath of imagePaths) { await fs.unlink(imagePath).catch(() => {}); } res.json({ success: true, operacaoId, outputPath, imagesProcessed: files.length, pdfSize: Number(tamanhoResultado), processingTime: tempoProcessamento, }); } catch (error) { console.error('Erro ao converter imagens para PDF:', error); if (operacaoId) { await prisma.operacaoPDF.update({ where: { id: operacaoId }, data: { status: 'ERRO', mensagemErro: error instanceof Error ? error.message : 'Erro desconhecido', tempoProcessamento: Date.now() - startTime, }, }); } res.status(500).json({ error: 'Falha ao converter imagens para PDF', message: error instanceof Error ? error.message : 'Erro desconhecido', }); } } /** * Realiza OCR em PDF */ static async performOCR(req: Request, res: Response): Promise { const startTime = Date.now(); let operacaoId: string | null = null; try { const file = (req.files as Express.Multer.File[])?.[0]; const userId = (req as any).user?.id; if (!file) { res.status(400).json({ error: 'Nenhum arquivo enviado' }); return; } if (!userId) { res.status(401).json({ error: 'Usuário não autenticado' }); return; } const language = req.body.language || 'por'; const pages = req.body.pages ? JSON.parse(req.body.pages) : 'all'; // Cria registro da operação const operacao = await prisma.operacaoPDF.create({ data: { tipoOperacao: 'OCR', status: 'PROCESSANDO', usuarioId: userId, arquivosOriginais: [file.path], arquivosResultado: [], tamanhoOriginal: BigInt(file.size), parametros: { language, pages }, }, }); operacaoId = operacao.id; // Realiza OCR const ocrResult = await PDFService.performOCR(file.path, { language, pages, }); const tempoProcessamento = Date.now() - startTime; // Atualiza a operação await prisma.operacaoPDF.update({ where: { id: operacaoId }, data: { status: 'CONCLUIDO', detalhesOCR: ocrResult as any, tempoProcessamento, }, }); // Remove o arquivo original await fs.unlink(file.path).catch(() => {}); res.json({ success: true, operacaoId, result: ocrResult, processingTime: tempoProcessamento, }); } catch (error) { console.error('Erro ao realizar OCR:', error); if (operacaoId) { await prisma.operacaoPDF.update({ where: { id: operacaoId }, data: { status: 'ERRO', mensagemErro: error instanceof Error ? error.message : 'Erro desconhecido', tempoProcessamento: Date.now() - startTime, }, }); } res.status(500).json({ error: 'Falha ao realizar OCR', message: error instanceof Error ? error.message : 'Erro desconhecido', }); } } /** * Obtém informações sobre um PDF */ static async getInfo(req: Request, res: Response): Promise { try { const file = (req.files as Express.Multer.File[])?.[0]; if (!file) { res.status(400).json({ error: 'Nenhum arquivo enviado' }); return; } const info = await PDFService.getPDFInfo(file.path); // Remove o arquivo await fs.unlink(file.path).catch(() => {}); res.json({ success: true, info, }); } catch (error) { console.error('Erro ao obter informações do PDF:', error); res.status(500).json({ error: 'Falha ao obter informações do PDF', message: error instanceof Error ? error.message : 'Erro desconhecido', }); } } /** * Obtém histórico de operações do usuário */ static async getHistory(req: Request, res: Response): Promise { try { const userId = (req as any).user?.id; if (!userId) { res.status(401).json({ error: 'Usuário não autenticado' }); return; } const page = parseInt(req.query.page as string) || 1; const limit = parseInt(req.query.limit as string) || 20; const skip = (page - 1) * limit; const [operacoes, total] = await Promise.all([ prisma.operacaoPDF.findMany({ where: { usuarioId: userId }, orderBy: { criadoEm: 'desc' }, skip, take: limit, select: { id: true, tipoOperacao: true, status: true, tamanhoOriginal: true, tamanhoResultado: true, tempoProcessamento: true, mensagemErro: true, criadoEm: true, parametros: true, }, }), prisma.operacaoPDF.count({ where: { usuarioId: userId }, }), ]); res.json({ success: true, operacoes, pagination: { page, limit, total, pages: Math.ceil(total / limit), }, }); } catch (error) { console.error('Erro ao obter histórico:', error); res.status(500).json({ error: 'Falha ao obter histórico', message: error instanceof Error ? error.message : 'Erro desconhecido', }); } } /** * Download de arquivo processado */ static async downloadFile(req: Request, res: Response): Promise { try { const { operacaoId, fileIndex } = req.params; const userId = (req as any).user?.id; if (!userId) { res.status(401).json({ error: 'Usuário não autenticado' }); return; } const operacao = await prisma.operacaoPDF.findUnique({ where: { id: operacaoId }, }); if (!operacao) { res.status(404).json({ error: 'Operação não encontrada' }); return; } if (operacao.usuarioId !== userId) { res.status(403).json({ error: 'Acesso negado' }); return; } const index = parseInt(fileIndex); const filePath = operacao.arquivosResultado[index]; if (!filePath) { res.status(404).json({ error: 'Arquivo não encontrado' }); return; } res.download(filePath, path.basename(filePath)); } catch (error) { console.error('Erro ao fazer download:', error); res.status(500).json({ error: 'Falha ao fazer download', message: error instanceof Error ? error.message : 'Erro desconhecido', }); } } }