pdf: fix feature

Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo.sousa.577@gmail.com>
This commit is contained in:
rodrigo committed 2026-02-27 20:15:07 -03:00
1 parent f4afe83db0
commit db7b832044
11 files changed
+1009 -104

No files matched your search

+298 -7
View File
@@ -70,8 +70,9 @@ export class PDFController {
return;
}
const quality = req.body.quality ? parseInt(req.body.quality) : 95;
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[] = [];
@@ -99,7 +100,23 @@ export class PDFController {
arquivosOriginais.push(file.path);
tamanhoOriginal += BigInt(file.size);
const result = await PDFService.compressPDF(file.path, undefined, {
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,
});
@@ -115,6 +132,10 @@ export class PDFController {
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(() => {});
}
@@ -221,6 +242,16 @@ export class PDFController {
// 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) {
@@ -235,7 +266,7 @@ export class PDFController {
where: { id: operacaoId },
data: {
status: 'CONCLUIDO',
arquivosResultado: outputPaths,
arquivosResultado: zipPath ? [zipPath, ...outputPaths] : outputPaths,
tamanhoResultado,
tempoProcessamento,
},
@@ -249,6 +280,7 @@ export class PDFController {
operacaoId,
filesCreated: outputPaths.length,
outputPaths,
zipPath,
processingTime: tempoProcessamento,
});
} catch (error) {
@@ -293,6 +325,7 @@ export class PDFController {
return;
}
const enableOCR = req.body.enableOCR === 'true' || req.body.enableOCR === true;
const filePaths = files.map((f) => f.path);
let tamanhoOriginal = BigInt(0);
@@ -316,11 +349,23 @@ export class PDFController {
operacaoId = operacao.id;
// Mescla os PDFs
const outputPath = await PDFService.mergePDFs({
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);
@@ -422,6 +467,16 @@ export class PDFController {
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) {
@@ -436,7 +491,7 @@ export class PDFController {
where: { id: operacaoId },
data: {
status: 'CONCLUIDO',
arquivosResultado: outputPaths,
arquivosResultado: zipPath ? [zipPath, ...outputPaths] : outputPaths,
tamanhoResultado,
tempoProcessamento,
},
@@ -450,6 +505,7 @@ export class PDFController {
operacaoId,
imagesCreated: outputPaths.length,
outputPaths,
zipPath,
processingTime: tempoProcessamento,
});
} catch (error) {
@@ -503,6 +559,7 @@ export class PDFController {
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({
@@ -520,13 +577,25 @@ export class PDFController {
operacaoId = operacao.id;
// Converte para PDF
const outputPath = await PDFService.imagesToPDF({
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);
@@ -797,7 +866,37 @@ export class PDFController {
return;
}
res.download(filePath, path.basename(filePath));
// 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({
@@ -806,4 +905,196 @@ export class PDFController {
});
}
}
/**
* 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',
});
}
}
}