| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- 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;
|