pdf: fix feature
Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo.sousa.577@gmail.com>
This commit is contained in:
1 parent
f4afe83db0
commit
db7b832044
11 files changed
+1009
-104
No files matched your search
@@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user