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:
parent
b3bca576da
commit
15485bb405
@ -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 };
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
@ -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 };
|
||||
Loading…
Reference in New Issue
Block a user