From d7b93325d4f70eeab369fa8a3c4cce9f0c4e0081 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Thu, 27 Aug 2026 19:03:34 +0200 Subject: Add dual GPX/FIT exporter and backfill for rides Phase 1 writes both .gpx and .fit on Strava import, adds fit_path column, renames storage.gpx_dir to storage.rides_dir, and backfills missing FIT files from existing GPX without breaking CLI progress. FIT files are gitignored. --- web/sync.go | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) (limited to 'web/sync.go') diff --git a/web/sync.go b/web/sync.go index f6dc7da..8ed306f 100644 --- a/web/sync.go +++ b/web/sync.go @@ -78,7 +78,7 @@ func (s *Server) handleSync(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Content-Type", "text/event-stream") - progress, err := s.syncRides(client, appConfig.Storage.GPXDir, from, to, func(progress SyncProgress) error { + progress, err := s.syncRides(client, appConfig.Storage.RidesDir, from, to, func(progress SyncProgress) error { enrichProgressRateLimit(&progress, client) return writeSyncEvent(w, flusher, "progress", progress) }) @@ -104,7 +104,7 @@ func (s *Server) handleSync(w http.ResponseWriter, r *http.Request) { _ = writeSyncEvent(w, flusher, "complete", progress) } -func (s *Server) syncRides(client syncClient, gpxDir string, from, to time.Time, report func(SyncProgress) error) (SyncProgress, error) { +func (s *Server) syncRides(client syncClient, ridesDir string, from, to time.Time, report func(SyncProgress) error) (SyncProgress, error) { activities, err := client.List(from, to) if err != nil { if rateErr, ok := errors.AsType[*strava.RateLimitError](err); ok { @@ -117,8 +117,8 @@ func (s *Server) syncRides(client syncClient, gpxDir string, from, to time.Time, return progress, err } slog.Info("Started Strava sync", "from", from.Format(dateFormat), "to", to.Add(-24*time.Hour).Format(dateFormat), "total", progress.Total) - if err := os.MkdirAll(gpxDir, 0o755); err != nil { - return progress, fmt.Errorf("create GPX directory: %w", err) + if err := os.MkdirAll(ridesDir, 0o755); err != nil { + return progress, fmt.Errorf("create rides directory: %w", err) } for _, summary := range activities { externalID := fmt.Sprintf("strava:%d", summary.ID) @@ -146,15 +146,16 @@ func (s *Server) syncRides(client syncClient, gpxDir string, from, to time.Time, } continue } - activity, gpxData, err := client.Get(summary.ID) + activity, gpxData, fitData, err := client.Get(summary.ID) if err != nil { if rateErr, ok := errors.AsType[*strava.RateLimitError](err); ok { slog.Warn("Strava sync rate limited", "activity", summary.ID, "name", summary.Name, "retryAfter", rateErr.RetryAfter, "isDaily", rateErr.IsDaily, "completed", progress.Completed, "total", progress.Total) return progress, err } if errors.Is(err, strava.ErrNoTrackPoints) { - _ = os.Remove(filepath.Join(gpxDir, fmt.Sprintf("activity_%d.gpx", summary.ID))) - if saveErr := ride.Save(s.db, rideFromActivity(externalID, "", activity)); saveErr != nil { + _ = os.Remove(filepath.Join(ridesDir, fmt.Sprintf("activity_%d.gpx", summary.ID))) + _ = os.Remove(filepath.Join(ridesDir, fmt.Sprintf("activity_%d.fit", summary.ID))) + if saveErr := ride.Save(s.db, rideFromActivity(externalID, "", "", activity)); saveErr != nil { if reportErr := skipSyncActivity(&progress, report, summary, fmt.Errorf("save metadata: %w", saveErr)); reportErr != nil { return progress, reportErr } @@ -173,16 +174,27 @@ func (s *Server) syncRides(client syncClient, gpxDir string, from, to time.Time, } continue } - gpxPath := filepath.Join(gpxDir, fmt.Sprintf("activity_%d.gpx", activity.ID)) + gpxPath := filepath.Join(ridesDir, fmt.Sprintf("activity_%d.gpx", activity.ID)) + fitPath := filepath.Join(ridesDir, fmt.Sprintf("activity_%d.fit", activity.ID)) if err := os.WriteFile(gpxPath, gpxData, 0o644); err != nil { _ = os.Remove(gpxPath) + _ = os.Remove(fitPath) if reportErr := skipSyncActivity(&progress, report, summary, fmt.Errorf("write GPX: %w", err)); reportErr != nil { return progress, reportErr } continue } - if err := ride.Save(s.db, rideFromActivity(externalID, gpxPath, activity)); err != nil { + if err := os.WriteFile(fitPath, fitData, 0o644); err != nil { _ = os.Remove(gpxPath) + _ = os.Remove(fitPath) + if reportErr := skipSyncActivity(&progress, report, summary, fmt.Errorf("write FIT: %w", err)); reportErr != nil { + return progress, reportErr + } + continue + } + if err := ride.Save(s.db, rideFromActivity(externalID, gpxPath, fitPath, activity)); err != nil { + _ = os.Remove(gpxPath) + _ = os.Remove(fitPath) if reportErr := skipSyncActivity(&progress, report, summary, fmt.Errorf("save activity: %w", err)); reportErr != nil { return progress, reportErr } @@ -198,7 +210,7 @@ func (s *Server) syncRides(client syncClient, gpxDir string, from, to time.Time, return progress, nil } -func rideFromActivity(externalID, gpxPath string, activity strava.Activity) ride.StoredRide { +func rideFromActivity(externalID, gpxPath, fitPath string, activity strava.Activity) ride.StoredRide { activityType := activity.SportType if activityType == "" { activityType = activity.Type @@ -206,6 +218,7 @@ func rideFromActivity(externalID, gpxPath string, activity strava.Activity) ride return ride.StoredRide{ ExternalID: externalID, GPXPath: gpxPath, + FitPath: fitPath, Name: activity.Name, Type: activityType, StartDate: activity.StartDate, -- cgit v1.3