tracks.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. const router = require('express').Router();
  2. const multer = require('multer');
  3. const { requireAuth } = require('../middleware/auth');
  4. const { Track, TrackPoint, Directory, ShareLink } = require('../models');
  5. const { parseAndCompress } = require('../utils/gpx-processor');
  6. const { generateCode } = require('../utils/shortcode');
  7. let config;
  8. try { config = require('../../config'); } catch(e) { config = { upload: { maxFileSizeMB: 50 } }; }
  9. const upload = multer({
  10. storage: multer.memoryStorage(),
  11. limits: { fileSize: (config.upload?.maxFileSizeMB || 50) * 1024 * 1024 },
  12. fileFilter: (req, file, cb) => {
  13. if (file.originalname.toLowerCase().endsWith('.gpx') || file.mimetype === 'application/gpx+xml' || file.mimetype === 'text/xml' || file.mimetype === 'application/xml') {
  14. cb(null, true);
  15. } else {
  16. cb(new Error('Only GPX files allowed'));
  17. }
  18. }
  19. });
  20. router.get('/', requireAuth, async (req, res) => {
  21. try {
  22. const where = { userId: req.user.id };
  23. if (req.query.directoryId !== undefined) {
  24. where.directoryId = req.query.directoryId || null;
  25. }
  26. const tracks = await Track.findAll({
  27. where,
  28. order: [['trackDate', 'DESC'], ['uploadDate', 'DESC']],
  29. attributes: ['id', 'name', 'originalFilename', 'uploadDate', 'trackDate', 'pointCount', 'totalDistance', 'directoryId', 'trackType'],
  30. });
  31. res.json(tracks);
  32. } catch (e) {
  33. res.status(500).json({ error: 'Server error' });
  34. }
  35. });
  36. router.get('/:id', requireAuth, async (req, res) => {
  37. try {
  38. const track = await Track.findOne({
  39. where: { id: req.params.id, userId: req.user.id },
  40. include: [{ model: ShareLink, attributes: ['code'] }],
  41. });
  42. if (!track) return res.status(404).json({ error: 'Track not found' });
  43. res.json(track);
  44. } catch (e) {
  45. res.status(500).json({ error: 'Server error' });
  46. }
  47. });
  48. router.get('/:id/points', requireAuth, async (req, res) => {
  49. try {
  50. const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } });
  51. if (!track) return res.status(404).json({ error: 'Track not found' });
  52. const points = await TrackPoint.findAll({
  53. where: { trackId: track.id },
  54. order: [['segmentId', 'ASC'], ['sequence', 'ASC']],
  55. attributes: ['lat', 'lon', 'elevation', 'time', 'segmentId'],
  56. });
  57. // Group by segment
  58. const segMap = {};
  59. for (const p of points) {
  60. if (!segMap[p.segmentId]) segMap[p.segmentId] = [];
  61. segMap[p.segmentId].push([p.lat, p.lon, p.elevation, p.time]);
  62. }
  63. const segments = Object.keys(segMap).sort((a,b)=>a-b).map(k => segMap[k]);
  64. res.json({
  65. meta: { trackId: track.id, name: track.name, totalDistance: track.totalDistance, pointCount: track.pointCount, trackDate: track.trackDate, trackType: track.trackType },
  66. segments
  67. });
  68. } catch (e) {
  69. res.status(500).json({ error: 'Server error' });
  70. }
  71. });
  72. router.post('/upload', requireAuth, (req, res, next) => {
  73. upload.single('file')(req, res, (err) => {
  74. if (err) return res.status(400).json({ error: err.message || 'File upload error' });
  75. next();
  76. });
  77. }, async (req, res) => {
  78. try {
  79. if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
  80. const xmlString = req.file.buffer.toString('utf8');
  81. const { segments, trackName, trackDate, totalDistance, pointCount } = await parseAndCompress(xmlString);
  82. if (pointCount === 0) return res.status(400).json({ error: 'No valid track points found in GPX file' });
  83. const directoryId = req.body.directoryId ? parseInt(req.body.directoryId) : null;
  84. if (directoryId) {
  85. const dir = await Directory.findOne({ where: { id: directoryId, userId: req.user.id } });
  86. if (!dir) return res.status(404).json({ error: 'Directory not found' });
  87. }
  88. const name = req.body.name || trackName || req.file.originalname.replace(/\.gpx$/i, '');
  89. const track = await Track.create({
  90. userId: req.user.id,
  91. directoryId,
  92. name,
  93. originalFilename: req.file.originalname,
  94. uploadDate: new Date(),
  95. trackDate: trackDate || null,
  96. pointCount,
  97. totalDistance,
  98. });
  99. // Bulk insert points
  100. const allPoints = [];
  101. for (const seg of segments) {
  102. for (const pt of seg.points) {
  103. allPoints.push({
  104. trackId: track.id,
  105. lat: pt.lat,
  106. lon: pt.lon,
  107. elevation: pt.elevation,
  108. time: pt.time,
  109. sequence: pt.sequence,
  110. segmentId: pt.segmentId,
  111. });
  112. }
  113. }
  114. await TrackPoint.bulkCreate(allPoints);
  115. // Update parent directory updatedAt
  116. if (directoryId) {
  117. await Directory.update({ updatedAt: new Date() }, { where: { id: directoryId } });
  118. }
  119. res.status(201).json(track);
  120. } catch (e) {
  121. console.error(e);
  122. res.status(500).json({ error: e.message || 'Server error' });
  123. }
  124. });
  125. router.put('/:id', requireAuth, async (req, res) => {
  126. try {
  127. const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } });
  128. if (!track) return res.status(404).json({ error: 'Track not found' });
  129. const VALID_TYPES = ['hiking', 'running', 'cycling', 'driving', 'train', 'other', null, ''];
  130. const updates = {};
  131. if (req.body.name) updates.name = req.body.name;
  132. if ('trackType' in req.body) {
  133. const t = req.body.trackType || null;
  134. if (t !== null && !VALID_TYPES.includes(t))
  135. return res.status(400).json({ error: 'Invalid trackType' });
  136. updates.trackType = t;
  137. }
  138. if (req.body.directoryId !== undefined) {
  139. const dirId = req.body.directoryId || null;
  140. if (dirId) {
  141. const dir = await Directory.findOne({ where: { id: dirId, userId: req.user.id } });
  142. if (!dir) return res.status(404).json({ error: 'Target directory not found' });
  143. }
  144. updates.directoryId = dirId;
  145. }
  146. await track.update(updates);
  147. res.json(track);
  148. } catch (e) {
  149. res.status(500).json({ error: 'Server error' });
  150. }
  151. });
  152. router.delete('/:id', requireAuth, async (req, res) => {
  153. try {
  154. const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } });
  155. if (!track) return res.status(404).json({ error: 'Track not found' });
  156. // Delete children explicitly — don't rely on DB-level cascade
  157. await TrackPoint.destroy({ where: { trackId: track.id } });
  158. await ShareLink.destroy({ where: { trackId: track.id } });
  159. await track.destroy();
  160. res.json({ ok: true });
  161. } catch (e) {
  162. res.status(500).json({ error: 'Server error' });
  163. }
  164. });
  165. // Share link
  166. router.post('/:id/share', requireAuth, async (req, res) => {
  167. try {
  168. const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } });
  169. if (!track) return res.status(404).json({ error: 'Track not found' });
  170. let link = await ShareLink.findOne({ where: { trackId: track.id } });
  171. if (!link) {
  172. let code, tries = 0;
  173. do {
  174. code = generateCode();
  175. const existing = await ShareLink.findOne({ where: { code } });
  176. if (!existing) break;
  177. tries++;
  178. } while (tries < 10);
  179. link = await ShareLink.create({ trackId: track.id, code });
  180. }
  181. res.json({ code: link.code });
  182. } catch (e) {
  183. res.status(500).json({ error: 'Server error' });
  184. }
  185. });
  186. router.delete('/:id/share', requireAuth, async (req, res) => {
  187. try {
  188. const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } });
  189. if (!track) return res.status(404).json({ error: 'Track not found' });
  190. await ShareLink.destroy({ where: { trackId: track.id } });
  191. res.json({ ok: true });
  192. } catch (e) {
  193. res.status(500).json({ error: 'Server error' });
  194. }
  195. });
  196. module.exports = router;