1101 lines
31 KiB
TypeScript
1101 lines
31 KiB
TypeScript
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<void> {
|
|
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) : 50; // Padrão 50 para mais compressão
|
|
const maxSizeMB = req.body.maxSizeMB ? parseInt(req.body.maxSizeMB) : undefined;
|
|
const enableOCR = req.body.enableOCR === 'true' || req.body.enableOCR === true;
|
|
|
|
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);
|
|
|
|
let processedPath = file.path;
|
|
|
|
// Adiciona OCR se solicitado
|
|
if (enableOCR) {
|
|
try {
|
|
const ocrResult = await PDFService.addOCRLayerToPDF(file.path);
|
|
processedPath = ocrResult.outputPath;
|
|
// Remove arquivo temporário anterior
|
|
if (processedPath !== file.path) {
|
|
await fs.unlink(file.path).catch(() => {});
|
|
}
|
|
} catch (ocrError) {
|
|
console.warn('Erro ao adicionar OCR, continuando sem OCR:', ocrError);
|
|
}
|
|
}
|
|
|
|
const result = await PDFService.compressPDF(processedPath, 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 intermediário
|
|
if (processedPath !== file.path && processedPath !== result.outputPath) {
|
|
await fs.unlink(processedPath).catch(() => {});
|
|
}
|
|
// 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,
|
|
outputPaths: arquivosResultado,
|
|
totalOriginalSize: Number(tamanhoOriginal),
|
|
totalCompressedSize: Number(tamanhoResultado),
|
|
reduction: ((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<void> {
|
|
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);
|
|
|
|
// Cria ZIP se houver múltiplos arquivos
|
|
let zipPath: string | undefined;
|
|
if (outputPaths.length > 1) {
|
|
try {
|
|
zipPath = await PDFService.createZipFromFiles(outputPaths);
|
|
} catch (zipError) {
|
|
console.warn('Erro ao criar ZIP, disponibilizando arquivos individuais:', zipError);
|
|
}
|
|
}
|
|
|
|
// 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: zipPath ? [zipPath, ...outputPaths] : outputPaths,
|
|
tamanhoResultado,
|
|
tempoProcessamento,
|
|
},
|
|
});
|
|
|
|
// Remove o arquivo original
|
|
await fs.unlink(file.path).catch(() => {});
|
|
|
|
res.json({
|
|
success: true,
|
|
operacaoId,
|
|
filesCreated: outputPaths.length,
|
|
outputPaths,
|
|
zipPath,
|
|
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<void> {
|
|
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 enableOCR = req.body.enableOCR === 'true' || req.body.enableOCR === true;
|
|
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
|
|
let outputPath = await PDFService.mergePDFs({
|
|
filePaths,
|
|
outputPath: path.join(process.cwd(), 'storage', 'pdf-output', `merged-${uuidv4()}.pdf`),
|
|
});
|
|
|
|
// Adiciona OCR se solicitado
|
|
if (enableOCR) {
|
|
try {
|
|
const ocrResult = await PDFService.addOCRLayerToPDF(outputPath);
|
|
// Remove arquivo anterior
|
|
await fs.unlink(outputPath).catch(() => {});
|
|
outputPath = ocrResult.outputPath;
|
|
} catch (ocrError) {
|
|
console.warn('Erro ao adicionar OCR ao PDF mesclado:', ocrError);
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
outputPaths: [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<void> {
|
|
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,
|
|
});
|
|
|
|
// Cria ZIP com todas as imagens
|
|
let zipPath: string | undefined;
|
|
if (outputPaths.length > 0) {
|
|
try {
|
|
zipPath = await PDFService.createZipFromFiles(outputPaths);
|
|
} catch (zipError) {
|
|
console.warn('Erro ao criar ZIP, disponibilizando imagens individuais:', zipError);
|
|
}
|
|
}
|
|
|
|
// 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: zipPath ? [zipPath, ...outputPaths] : outputPaths,
|
|
tamanhoResultado,
|
|
tempoProcessamento,
|
|
},
|
|
});
|
|
|
|
// Remove o arquivo original
|
|
await fs.unlink(file.path).catch(() => {});
|
|
|
|
res.json({
|
|
success: true,
|
|
operacaoId,
|
|
imagesCreated: outputPaths.length,
|
|
outputPaths,
|
|
zipPath,
|
|
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<void> {
|
|
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;
|
|
const enableOCR = req.body.enableOCR === 'true' || req.body.enableOCR === true;
|
|
|
|
// 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
|
|
let outputPath = await PDFService.imagesToPDF({
|
|
imagePaths,
|
|
outputPath: path.join(process.cwd(), 'storage', 'pdf-output', `images-to-pdf-${uuidv4()}.pdf`),
|
|
fitToPage,
|
|
pageSize,
|
|
});
|
|
|
|
// Adiciona OCR se solicitado
|
|
if (enableOCR) {
|
|
try {
|
|
const ocrResult = await PDFService.addOCRLayerToPDF(outputPath);
|
|
// Remove arquivo anterior
|
|
await fs.unlink(outputPath).catch(() => {});
|
|
outputPath = ocrResult.outputPath;
|
|
} catch (ocrError) {
|
|
console.warn('Erro ao adicionar OCR ao PDF:', ocrError);
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
outputPaths: [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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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,
|
|
arquivosResultado: true,
|
|
},
|
|
}),
|
|
prisma.operacaoPDF.count({
|
|
where: { usuarioId: userId },
|
|
}),
|
|
]);
|
|
|
|
// Converter BigInt para Number para evitar erro de serialização JSON
|
|
const operacoesSerializadas = operacoes.map(op => ({
|
|
...op,
|
|
tamanhoOriginal: Number(op.tamanhoOriginal),
|
|
tamanhoResultado: op.tamanhoResultado ? Number(op.tamanhoResultado) : null,
|
|
}));
|
|
|
|
res.json({
|
|
success: true,
|
|
operacoes: operacoesSerializadas,
|
|
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<void> {
|
|
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;
|
|
}
|
|
|
|
// Verifica se o arquivo existe
|
|
try {
|
|
await fs.access(filePath);
|
|
} catch {
|
|
res.status(404).json({ error: 'Arquivo não encontrado no sistema' });
|
|
return;
|
|
}
|
|
|
|
// Detecta o Content-Type correto baseado na extensão
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
const mimeTypes: { [key: string]: string } = {
|
|
'.pdf': 'application/pdf',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.webp': 'image/webp',
|
|
'.zip': 'application/zip',
|
|
};
|
|
|
|
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
const fileName = path.basename(filePath);
|
|
|
|
console.log(`Download solicitado: ${fileName} (${contentType})`);
|
|
|
|
// Define o Content-Type correto
|
|
res.setHeader('Content-Type', contentType);
|
|
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
|
|
|
// Garante caminho absoluto e envia o arquivo
|
|
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
|
|
res.sendFile(absolutePath);
|
|
} 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',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deleta uma operação específica e seus arquivos
|
|
*/
|
|
static async deleteOperation(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { operacaoId } = 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;
|
|
}
|
|
|
|
// Deleta arquivos físicos
|
|
const allFiles = [...operacao.arquivosOriginais, ...operacao.arquivosResultado];
|
|
for (const filePath of allFiles) {
|
|
try {
|
|
// Verifica se o arquivo existe antes de deletar
|
|
await fs.access(filePath);
|
|
await fs.unlink(filePath);
|
|
} catch (error: any) {
|
|
// Ignora erro se o arquivo não existe (já foi deletado)
|
|
if (error.code !== 'ENOENT') {
|
|
console.warn(`Erro ao deletar arquivo ${filePath}:`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deleta registro do banco
|
|
await prisma.operacaoPDF.delete({
|
|
where: { id: operacaoId },
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Operação deletada com sucesso',
|
|
});
|
|
} catch (error) {
|
|
console.error('Erro ao deletar operação:', error);
|
|
res.status(500).json({
|
|
error: 'Falha ao deletar operação',
|
|
message: error instanceof Error ? error.message : 'Erro desconhecido',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Limpa todo o histórico do usuário
|
|
*/
|
|
static async clearHistory(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const userId = (req as any).user?.id;
|
|
|
|
if (!userId) {
|
|
res.status(401).json({ error: 'Usuário não autenticado' });
|
|
return;
|
|
}
|
|
|
|
// Busca todas as operações do usuário
|
|
const operacoes = await prisma.operacaoPDF.findMany({
|
|
where: { usuarioId: userId },
|
|
});
|
|
|
|
let filesDeleted = 0;
|
|
let filesError = 0;
|
|
|
|
// Deleta todos os arquivos
|
|
for (const operacao of operacoes) {
|
|
const allFiles = [...operacao.arquivosOriginais, ...operacao.arquivosResultado];
|
|
for (const filePath of allFiles) {
|
|
try {
|
|
// Verifica se o arquivo existe antes de deletar
|
|
await fs.access(filePath);
|
|
await fs.unlink(filePath);
|
|
filesDeleted++;
|
|
} catch (error: any) {
|
|
// Ignora erro se o arquivo não existe
|
|
if (error.code !== 'ENOENT') {
|
|
filesError++;
|
|
console.warn(`Erro ao deletar arquivo ${filePath}:`, error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deleta todos os registros
|
|
const deleted = await prisma.operacaoPDF.deleteMany({
|
|
where: { usuarioId: userId },
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Histórico limpo com sucesso',
|
|
operacoesRemovidas: deleted.count,
|
|
arquivosDeletados: filesDeleted,
|
|
arquivosComErro: filesError,
|
|
});
|
|
} catch (error) {
|
|
console.error('Erro ao limpar histórico:', error);
|
|
res.status(500).json({
|
|
error: 'Falha ao limpar histórico',
|
|
message: error instanceof Error ? error.message : 'Erro desconhecido',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Limpa operações antigas (mais de X horas)
|
|
*/
|
|
static async cleanupOldOperations(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const userId = (req as any).user?.id;
|
|
const hours = parseInt(req.query.hours as string) || 24;
|
|
|
|
if (!userId) {
|
|
res.status(401).json({ error: 'Usuário não autenticado' });
|
|
return;
|
|
}
|
|
|
|
const cutoffDate = new Date(Date.now() - hours * 60 * 60 * 1000);
|
|
|
|
// Busca operações antigas
|
|
const operacoes = await prisma.operacaoPDF.findMany({
|
|
where: {
|
|
usuarioId: userId,
|
|
criadoEm: {
|
|
lt: cutoffDate,
|
|
},
|
|
},
|
|
});
|
|
|
|
let filesDeleted = 0;
|
|
let filesError = 0;
|
|
|
|
// Deleta arquivos
|
|
for (const operacao of operacoes) {
|
|
const allFiles = [...operacao.arquivosOriginais, ...operacao.arquivosResultado];
|
|
for (const filePath of allFiles) {
|
|
try {
|
|
// Verifica se o arquivo existe antes de deletar
|
|
await fs.access(filePath);
|
|
await fs.unlink(filePath);
|
|
filesDeleted++;
|
|
} catch (error: any) {
|
|
// Ignora erro se o arquivo não existe
|
|
if (error.code !== 'ENOENT') {
|
|
filesError++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deleta registros
|
|
const deleted = await prisma.operacaoPDF.deleteMany({
|
|
where: {
|
|
usuarioId: userId,
|
|
criadoEm: {
|
|
lt: cutoffDate,
|
|
},
|
|
},
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `Operações com mais de ${hours}h removidas`,
|
|
operacoesRemovidas: deleted.count,
|
|
arquivosDeletados: filesDeleted,
|
|
arquivosComErro: filesError,
|
|
});
|
|
} catch (error) {
|
|
console.error('Erro ao limpar operações antig as:', error);
|
|
res.status(500).json({
|
|
error: 'Falha ao limpar operações antigas',
|
|
message: error instanceof Error ? error.message : 'Erro desconhecido',
|
|
});
|
|
}
|
|
}
|
|
}
|