const router = require('express').Router(); const multer = require('multer'); const { requireAuth } = require('../middleware/auth'); const { Track, TrackPoint, Directory, ShareLink } = require('../models'); const { parseAndCompress } = require('../utils/gpx-processor'); const { generateCode } = require('../utils/shortcode'); let config; try { config = require('../../config'); } catch(e) { config = { upload: { maxFileSizeMB: 50 } }; } const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: (config.upload?.maxFileSizeMB || 50) * 1024 * 1024 }, fileFilter: (req, file, cb) => { if (file.originalname.toLowerCase().endsWith('.gpx') || file.mimetype === 'application/gpx+xml' || file.mimetype === 'text/xml' || file.mimetype === 'application/xml') { cb(null, true); } else { cb(new Error('Only GPX files allowed')); } } }); router.get('/', requireAuth, async (req, res) => { try { const where = { userId: req.user.id }; if (req.query.directoryId !== undefined) { where.directoryId = req.query.directoryId || null; } const tracks = await Track.findAll({ where, order: [['trackDate', 'DESC'], ['uploadDate', 'DESC']], attributes: ['id', 'name', 'originalFilename', 'uploadDate', 'trackDate', 'pointCount', 'totalDistance', 'directoryId', 'trackType'], }); res.json(tracks); } catch (e) { res.status(500).json({ error: 'Server error' }); } }); router.get('/:id', requireAuth, async (req, res) => { try { const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id }, include: [{ model: ShareLink, attributes: ['code'] }], }); if (!track) return res.status(404).json({ error: 'Track not found' }); res.json(track); } catch (e) { res.status(500).json({ error: 'Server error' }); } }); router.get('/:id/points', requireAuth, async (req, res) => { try { const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } }); if (!track) return res.status(404).json({ error: 'Track not found' }); const points = await TrackPoint.findAll({ where: { trackId: track.id }, order: [['segmentId', 'ASC'], ['sequence', 'ASC']], attributes: ['lat', 'lon', 'elevation', 'time', 'segmentId'], }); // Group by segment const segMap = {}; for (const p of points) { if (!segMap[p.segmentId]) segMap[p.segmentId] = []; segMap[p.segmentId].push([p.lat, p.lon, p.elevation, p.time]); } const segments = Object.keys(segMap).sort((a,b)=>a-b).map(k => segMap[k]); res.json({ meta: { trackId: track.id, name: track.name, totalDistance: track.totalDistance, pointCount: track.pointCount, trackDate: track.trackDate, trackType: track.trackType }, segments }); } catch (e) { res.status(500).json({ error: 'Server error' }); } }); router.post('/upload', requireAuth, (req, res, next) => { upload.single('file')(req, res, (err) => { if (err) return res.status(400).json({ error: err.message || 'File upload error' }); next(); }); }, async (req, res) => { try { if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); const xmlString = req.file.buffer.toString('utf8'); const { segments, trackName, trackDate, totalDistance, pointCount } = await parseAndCompress(xmlString); if (pointCount === 0) return res.status(400).json({ error: 'No valid track points found in GPX file' }); const directoryId = req.body.directoryId ? parseInt(req.body.directoryId) : null; if (directoryId) { const dir = await Directory.findOne({ where: { id: directoryId, userId: req.user.id } }); if (!dir) return res.status(404).json({ error: 'Directory not found' }); } const name = req.body.name || trackName || req.file.originalname.replace(/\.gpx$/i, ''); const track = await Track.create({ userId: req.user.id, directoryId, name, originalFilename: req.file.originalname, uploadDate: new Date(), trackDate: trackDate || null, pointCount, totalDistance, }); // Bulk insert points const allPoints = []; for (const seg of segments) { for (const pt of seg.points) { allPoints.push({ trackId: track.id, lat: pt.lat, lon: pt.lon, elevation: pt.elevation, time: pt.time, sequence: pt.sequence, segmentId: pt.segmentId, }); } } await TrackPoint.bulkCreate(allPoints); // Update parent directory updatedAt if (directoryId) { await Directory.update({ updatedAt: new Date() }, { where: { id: directoryId } }); } res.status(201).json(track); } catch (e) { console.error(e); res.status(500).json({ error: e.message || 'Server error' }); } }); router.put('/:id', requireAuth, async (req, res) => { try { const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } }); if (!track) return res.status(404).json({ error: 'Track not found' }); const VALID_TYPES = ['hiking', 'running', 'cycling', 'driving', 'train', 'other', null, '']; const updates = {}; if (req.body.name) updates.name = req.body.name; if ('trackType' in req.body) { const t = req.body.trackType || null; if (t !== null && !VALID_TYPES.includes(t)) return res.status(400).json({ error: 'Invalid trackType' }); updates.trackType = t; } if (req.body.directoryId !== undefined) { const dirId = req.body.directoryId || null; if (dirId) { const dir = await Directory.findOne({ where: { id: dirId, userId: req.user.id } }); if (!dir) return res.status(404).json({ error: 'Target directory not found' }); } updates.directoryId = dirId; } await track.update(updates); res.json(track); } catch (e) { res.status(500).json({ error: 'Server error' }); } }); router.delete('/:id', requireAuth, async (req, res) => { try { const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } }); if (!track) return res.status(404).json({ error: 'Track not found' }); // Delete children explicitly — don't rely on DB-level cascade await TrackPoint.destroy({ where: { trackId: track.id } }); await ShareLink.destroy({ where: { trackId: track.id } }); await track.destroy(); res.json({ ok: true }); } catch (e) { res.status(500).json({ error: 'Server error' }); } }); // Share link router.post('/:id/share', requireAuth, async (req, res) => { try { const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } }); if (!track) return res.status(404).json({ error: 'Track not found' }); let link = await ShareLink.findOne({ where: { trackId: track.id } }); if (!link) { let code, tries = 0; do { code = generateCode(); const existing = await ShareLink.findOne({ where: { code } }); if (!existing) break; tries++; } while (tries < 10); link = await ShareLink.create({ trackId: track.id, code }); } res.json({ code: link.code }); } catch (e) { res.status(500).json({ error: 'Server error' }); } }); router.delete('/:id/share', requireAuth, async (req, res) => { try { const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } }); if (!track) return res.status(404).json({ error: 'Track not found' }); await ShareLink.destroy({ where: { trackId: track.id } }); res.json({ ok: true }); } catch (e) { res.status(500).json({ error: 'Server error' }); } }); module.exports = router;