Parcourir la source

Add track routes with GPX upload and share link management

k4be il y a 8 heures
Parent
commit
daaae9cee0
1 fichiers modifiés avec 205 ajouts et 0 suppressions
  1. 205 0
      gpx-vis-backend/src/routes/tracks.js

+ 205 - 0
gpx-vis-backend/src/routes/tracks.js

@@ -0,0 +1,205 @@
+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'],
+    });
+    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 },
+      segments
+    });
+  } catch (e) {
+    res.status(500).json({ error: 'Server error' });
+  }
+});
+
+router.post('/upload', requireAuth, upload.single('file'), 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 updates = {};
+    if (req.body.name) updates.name = req.body.name;
+    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' });
+    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;