const Busboy = require('busboy'); const { parse } = require('csv-parse/sync'); async function middleware(req, res, next) { const contentType = req.headers['content-type'] || ''; let csvText = null; let filename = null; if (req.params.filename && contentType.startsWith('text/csv')) { csvText = typeof req.body === 'string' ? req.body : req.body.toString('utf8'); filename = req.params.filename; processCsv(); } else if (contentType.startsWith('multipart/form-data')) { const busboy = Busboy({ headers: req.headers }); let found = false; busboy.on('file', (name, file, info) => { if (found) { file.resume(); return; } if (info.mimeType === 'text/csv') { found = true; filename = info.filename || 'unnamed.csv'; csvText = ''; file.setEncoding('utf8'); file.on('data', (data) => { csvText += data; }); file.on('end', () => {}); } else { file.resume(); } }); busboy.on('field', () => {}); // Ignore fields busboy.on('finish', () => { if (!found) { return next(); } processCsv(); }); req.pipe(busboy); return; } else { return next(); } function processCsv() { let records; try { records = parse(csvText, { relax_quotes: true, quote: '"', escape: '"', skip_empty_lines: true, trim: true }); } catch (e) { return res.status(400).json({ error: 'Invalid CSV' }); } if (!records.length) { return res.status(400).json({ error: 'Empty CSV' }); } const headers = records[0].map(h => h.toLowerCase().trim()); const rows = records.slice(1); let lastDate = null; let lastTime = null; const currentDate = new Date().toISOString().slice(0, 10); const currentTime = new Date().toISOString().slice(11, 19); const events = []; for (let row of rows) { let object = { labels: [] }; for (let k = 0; k < headers.length; k++) { let key = headers[k]; let val = row[k] ? row[k].trim() : ''; if (!key) continue; if (['remarks', 'event', 'comment', 'comments', 'text'].includes(key)) { object.remarks = val; } else if (key === 'label') { if (val) object.labels.push(val); } else if (key === 'labels') { if (val) object.labels.push(...val.split(';').map(l => l.trim()).filter(l => l)); } else if (key === 'sequence' || key === 'seq') { if (val) object.sequence = Number(val); } else if (['point', 'shot', 'shotpoint'].includes(key)) { if (val) object.point = Number(val); } else if (key === 'date') { object.date = val; } else if (key === 'time') { object.time = val; } else if (key === 'timestamp') { object.timestamp = val; } else if (key === 'latitude') { object.latitude = parseFloat(val); } else if (key === 'longitude') { object.longitude = parseFloat(val); } } if (!object.remarks) continue; let useSeqPoint = Number.isFinite(object.sequence) && Number.isFinite(object.point); let tstamp = null; if (!useSeqPoint) { if (object.timestamp) { tstamp = new Date(object.timestamp); } if (!tstamp || isNaN(tstamp.getTime())) { let dateStr = object.date || lastDate || currentDate; let timeStr = object.time || lastTime || currentTime; if (timeStr.length === 5) timeStr += ':00'; let full = `${dateStr}T${timeStr}.000Z`; tstamp = new Date(full); if (isNaN(tstamp.getTime())) continue; } if (object.date) lastDate = object.date; if (object.time) lastTime = object.time; } let event = { remarks: object.remarks, labels: object.labels, meta: { author: "*CSVImport*", "*CSVImport*": { filename, tstamp: new Date().toISOString() } } }; if (!isNaN(object.latitude) && !isNaN(object.longitude)) { event.meta.geometry = { type: "Point", coordinates: [object.longitude, object.latitude] }; } if (useSeqPoint) { event.sequence = object.sequence; event.point = object.point; } else if (tstamp) { event.tstamp = tstamp.toISOString(); } else { continue; } events.push(event); } req.body = events; next(); } } module.exports = middleware;