FEAT: Adiciona controle de download para CSV processado e modelos, além de melhorias na lógica de upload e processamento de arquivos CSV

This commit is contained in:
tulioperdigao 2025-12-30 11:37:42 -03:00
parent b3bca576da
commit 15485bb405
4 changed files with 141 additions and 49 deletions

View File

@ -1,6 +1,8 @@
const { consultarViabilidade } = require('../service/viabilidadeService');
const { processCsvFile } = require('../service/csvService');
const { getJob } = require('../service/jobStore.service');
const { processCsvFile, countValidLines } = require('../service/csvService');
const { getJob, createJob } = require('../service/jobStore.service');
const fs = require('fs');
const path = require('path');
// Controlador para consultar viabilidade
@ -25,15 +27,27 @@ async function uploadCsvFile(req, res) {
const filePath = req.file.path;
const originalName = req.file.originalname || req.file.filename || 'input.csv';
const out = await processCsvFile(filePath, originalName);
// Conta as linhas válidas primeiro
const total = await countValidLines(filePath);
if (total === 0) {
return res.status(400).json({ error: 'Nenhuma linha válida encontrada no CSV. Verifique se há colunas CEP e Número.' });
}
// normaliza retorno (processCsvFile pode retornar string ou objeto)
const outputPath = (typeof out === 'string') ? out : (out && out.outputPath) || null;
// Cria o job
const jobId = createJob(total);
return res.json({ outputPath });
// Inicia o processamento em background (não aguarda)
processCsvFile(jobId, filePath, originalName).catch(err => {
console.error('Erro no processamento em background:', err);
// Em caso de erro, marca o job como falhado
require('../service/jobStore.service').failJob(jobId, err.message);
});
// Retorna o jobId imediatamente para acompanhamento em tempo real
return res.json({ jobId });
} catch (error) {
console.error("Erro ao processar CSV:", error && (error.message || error));
return res.status(500).json({ error: 'Erro ao processar CSV' });
console.error("Erro ao iniciar processamento do CSV:", error && (error.message || error));
return res.status(500).json({ error: 'Erro ao iniciar processamento do CSV' });
}
}
@ -51,4 +65,89 @@ async function getJobController(req, res) {
}
}
module.exports = { consultarViabilidadeController, uploadCsvFile, getJobController };
// Controlador para download do CSV processado
// Verifica se o job está concluído e serve o arquivo para download
async function downloadCsvController(req, res) {
try {
const jobId = req.params.jobId;
const job = getJob(jobId);
// Verifica se o job existe, está concluído e tem um link de download
if (!job) {
return res.status(404).json({ error: 'Job não encontrado' });
}
if (job.status !== 'done') {
return res.status(400).json({ error: 'Processamento ainda não concluído' });
}
if (!job.download) {
return res.status(404).json({ error: 'Arquivo de download não disponível' });
}
// Extrai o nome do arquivo do campo download (ex: '/download/processed_123.csv' -> 'processed_123.csv')
const filename = path.basename(job.download);
const filePath = path.join(__dirname, '..', 'outputs', filename);
// Verifica se o arquivo existe no sistema de arquivos
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'Arquivo não encontrado no servidor' });
}
// Inicia o download do arquivo
res.download(filePath, filename, (err) => {
if (err) {
console.error("Erro ao fazer download do arquivo:", err);
// Se o download falhar, envia erro (mas headers já podem ter sido enviados)
if (!res.headersSent) {
res.status(500).json({ error: 'Erro ao fazer download do arquivo' });
}
}
});
} catch (error) {
console.error("Erro no controlador de download:", error && (error.message || error));
if (!res.headersSent) {
res.status(500).json({ error: 'Erro interno no servidor' });
}
}
}
// Controlador para download dos modelos de CSV
// Aceita parâmetro :type ('cep' ou 'geo') para escolher qual modelo baixar
async function downloadModelController(req, res) {
try {
const type = req.params.type;
let filename;
// Define o nome do arquivo baseado no tipo
if (type === 'cep') {
filename = 'modelo.viabilidade-cep.csv';
} else if (type === 'geo') {
filename = 'modelo.viabilidade-geolocalizacao.csv';
} else {
return res.status(400).json({ error: 'Tipo de modelo inválido. Use "cep" ou "geo".' });
}
const filePath = path.join(__dirname, '..', 'models', filename);
// Verifica se o arquivo existe
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'Arquivo de modelo não encontrado' });
}
// Inicia o download do arquivo
res.download(filePath, filename, (err) => {
if (err) {
console.error("Erro ao fazer download do modelo:", err);
if (!res.headersSent) {
res.status(500).json({ error: 'Erro ao fazer download do modelo' });
}
}
});
} catch (error) {
console.error("Erro no controlador de download de modelo:", error && (error.message || error));
if (!res.headersSent) {
res.status(500).json({ error: 'Erro interno no servidor' });
}
}
}
module.exports = { consultarViabilidadeController, uploadCsvFile, getJobController, downloadCsvController, downloadModelController };

View File

@ -36,7 +36,7 @@ async function pollJob(jobId) {
if (j.status === 'done') {
bar.style.width = '100%';
bar.innerText = '100%';
resEl.innerHTML = `Concluído. <a href="${j.download}">Baixar CSV processado</a>`;
resEl.innerHTML = `Concluído. <a href="/download/${jobId}">Baixar CSV processado</a>`;
return;
}
if (j.status === 'error') {
@ -84,39 +84,29 @@ document.getElementById('btnConsultaCep').addEventListener('click', async () =>
}
});
// baixar modelo
// botão que inicia download de todos os modelos individualmente
// baixar modelos
// botão que inicia download dos dois modelos simultaneamente via endpoints
document.addEventListener("DOMContentLoaded", () => {
const btn = document.getElementById("card__button-download");
if (!btn) return;
btn.addEventListener("click", async (e) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
try {
const resp = await fetch("/download-models/list");
if (!resp.ok) throw new Error("Não foi possível obter lista de modelos");
const data = await resp.json();
const files = data.files || [];
if (!files.length) {
alert("Nenhum arquivo de modelo disponível");
return;
}
// Baixar modelo de CEP
const link1 = document.createElement('a');
link1.href = '/download-model/cep';
link1.download = 'modelo.viabilidade-cep.csv';
document.body.appendChild(link1);
link1.click();
link1.remove();
// Para cada arquivo, cria um <a> e clica nele para iniciar download individual
files.forEach((fname) => {
const url = "/download-model/" + encodeURIComponent(fname);
const a = document.createElement("a");
a.href = url;
a.download = fname;
// necessário anexar no DOM para funcionar em alguns navegadores
document.body.appendChild(a);
a.click();
a.remove();
});
} catch (err) {
console.error("Erro ao baixar modelos:", err);
alert("Erro ao iniciar downloads: " + (err.message || err));
}
// Baixar modelo de geolocalização
const link2 = document.createElement('a');
link2.href = '/download-model/geo';
link2.download = 'modelo.viabilidade-geolocalizacao.csv';
document.body.appendChild(link2);
link2.click();
link2.remove();
});
});

View File

@ -13,12 +13,13 @@ router.post('/viabilidade', viabilidadeController.consultarViabilidadeController
// rota de upload agora usa multer.single('csvfile')
router.post('/upload', upload.single('csvfile'), viabilidadeController.uploadCsvFile);
router.get('/status/:jobId', (req, res) => {
const job = viabilidadeController.getJobController(req.params.jobId);
if (!job) {
return res.status(404).json({ error: 'Job não encontrado' });
}
res.json(job);
});
// Rota para verificar status do job
router.get('/status/:jobId', viabilidadeController.getJobController);
// Rota para download do CSV processado
router.get('/download/:jobId', viabilidadeController.downloadCsvController);
// Rota para download dos modelos
router.get('/download-model/:type', viabilidadeController.downloadModelController);
module.exports = router;

View File

@ -46,9 +46,11 @@ async function countValidLines(inputPath) {
}
// nova função: processa CSV linha a linha, chama consultarViabilidade e gera CSV de saída
async function processCsvFile(inputPath) {
// Recebe jobId já criado no controller
async function processCsvFile(jobId, inputPath, originalName) {
const total = await countValidLines(inputPath);
const jobId = createJob(total);
// Job já criado no controller
// const jobId = createJob(total);
const baseName = path.parse(inputPath).name;
const outputFilename = `processed_${Date.now()}_${baseName}.csv`;
const outputPath = path.join(__dirname, '..', 'outputs', outputFilename);
@ -137,9 +139,9 @@ async function processCsvFile(inputPath) {
outStream.end();
await once(outStream, 'finish');
finishJob(jobId, `/download/${path.basename(outputPath)}`);
finishJob(jobId, path.basename(outputPath));
return { jobId, outputPath };
return outputPath;
}
module.exports = { processCsvFile };
module.exports = { processCsvFile, countValidLines };