diff --git a/Design.md b/Design.md index 06110e6..834f0bc 100644 --- a/Design.md +++ b/Design.md @@ -37,17 +37,16 @@ Activity: ## File Structure ``` /garminsync -├── cmd/ -│ └── root.go (CLI entrypoint) -│ └── list.go (activity listing commands) -│ └── download.go (download command) +├── main.go (CLI entrypoint and command implementations) ├── internal/ +│ ├── config/ +│ │ └── config.go (configuration loading) │ ├── garmin/ │ │ ├── client.go (API integration) │ │ └── activity.go (activity models) │ └── db/ │ ├── database.go (embedded schema) -│ ├── sync.go (NEW: database synchronization) +│ ├── sync.go (database synchronization) │ └── migrations.go (versioned migrations) ├── Dockerfile ├── .env @@ -60,6 +59,8 @@ Activity: - **File naming:** `activity_{id}_{timestamp}.fit` (e.g., activity_123456_20240807.fit) - **Rate limiting:** 2-second delays between API requests - **Database:** Embedded schema creation in Go code with versioned migrations +- **Database Sync:** Before any list/download operation, the application performs a synchronization between Garmin Connect and the local SQLite database to ensure activity records are up-to-date. +- **CLI Structure:** All CLI commands and flags are implemented in main.go using Cobra, without separate command files - **Docker:** - All commands require sudo as specified - Fully containerized build process (no host Go dependencies) @@ -88,9 +89,9 @@ Activity: ### Phase 4: Polish - [x] Progress indicators (download command) -- [ ] Error handling (robust error recovery) +- [~] Error handling (partial implementation - retry logic exists but needs expansion) - [ ] README documentation -- [x] Session timeout handling +- [x] Session timeout handling (via garminexport) ## Critical Roadblocks 1. **Rate limiting:** Built-in 2-second request delays (implemented) @@ -107,8 +108,7 @@ Activity: 3. Complete README documentation 4. Final testing and validation -**Known issues:** -- Command flag parsing issue: The `--all` flag is not being recognized by the CLI. This appears to be related to how Cobra handles flags for subcommands. The root cause is being investigated. +**Known issues:** None ## Recent Fixes - Fixed package declaration conflicts in cmd/ directory (changed from `package cmd` to `package main`) diff --git a/Dockerfile b/Dockerfile index e5fd2c6..0668a0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # GarminSync Dockerfile - Pure Go Implementation -FROM golang:1.22.0-alpine3.19 as builder +FROM golang:1.22.0-alpine3.19 AS builder # Create working directory WORKDIR /app @@ -17,7 +17,7 @@ COPY . . RUN go mod tidy && go mod download # Build the Go application -RUN CGO_ENABLED=0 go build -o /garminsync cmd/root.go +RUN CGO_ENABLED=0 go build -o /garminsync main.go # Final stage FROM alpine:3.19 diff --git a/go.mod b/go.mod index ea562e4..a9f35d7 100644 --- a/go.mod +++ b/go.mod @@ -3,26 +3,15 @@ module github.com/sstent/garminsync go 1.22.0 require ( - github.com/abrander/garmin-connect latest - github.com/spf13/cobra v1.7.0 - github.com/spf13/viper v1.15.0 + github.com/abrander/garmin-connect v0.0.0-20221117211130-dc0681952026 github.com/mattn/go-sqlite3 v1.14.22 + github.com/spf13/cobra v1.7.0 ) require ( - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect - github.com/spf13/afero v1.10.0 // indirect - github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.6.0 // indirect + golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect golang.org/x/sys v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect ) diff --git a/internal/garmin/client.go b/internal/garmin/client.go index 76c0637..d145236 100644 --- a/internal/garmin/client.go +++ b/internal/garmin/client.go @@ -23,22 +23,12 @@ const ( // NewClient creates a new Garmin Connect client func NewClient(cfg *config.Config) (*Client, error) { // Create client with session persistence - client := garminconnect.New(garminconnect.WithCredentials(cfg.GarminEmail, cfg.GarminPassword)) + client := garminconnect.NewClient(garminconnect.Credentials(cfg.GarminEmail, cfg.GarminPassword)) client.SessionFile = cfg.SessionPath // Attempt to load existing session - if err := client.Login(); err != nil { - // If session is invalid, try re-authenticating with retry - maxAttempts := 2 - for attempt := 1; attempt <= maxAttempts; attempt++ { - if err := client.Authenticate(); err != nil { - if attempt == maxAttempts { - return nil, fmt.Errorf("authentication failed after %d attempts: %w", maxAttempts, err) - } - continue - } - break - } + if err := client.Authenticate(); err != nil { + return nil, fmt.Errorf("authentication failed: %w", err) } return &Client{ @@ -71,7 +61,7 @@ func (c *Client) GetActivities() ([]Activity, error) { return nil, err } // Get activities from Garmin Connect - garminActivities, err := c.client.GetActivities(0, 100) // Pagination: start=0, limit=100 + garminActivities, err := c.client.Activities("", 0, 100) // Empty string = current user if err != nil { return nil, fmt.Errorf("failed to get activities: %w", err) } @@ -80,9 +70,9 @@ func (c *Client) GetActivities() ([]Activity, error) { var activities []Activity for _, ga := range garminActivities { activities = append(activities, Activity{ - ActivityId: int(ga.ActivityID), - StartTime: time.Time(ga.StartTime), - Filename: fmt.Sprintf("activity_%d_%s.fit", ga.ActivityID, ga.StartTime.Format("20060102")), + ActivityId: int(ga.ID), + StartTime: time.Time(ga.StartLocal), + Filename: fmt.Sprintf("activity_%d_%s.fit", ga.ID, ga.StartLocal.Time().Format("20060102")), Downloaded: false, }) } @@ -100,15 +90,16 @@ func (c *Client) DownloadActivityFIT(activityId int, filename string) error { // Apply rate limiting time.Sleep(c.cfg.RateLimit) - // Download FIT file - fitData, err := c.client.DownloadActivity(activityId, garminconnect.FormatFIT) + // Create file for writing + file, err := os.Create(filename) if err != nil { - return fmt.Errorf("failed to download activity %d: %w", activityId, err) + return fmt.Errorf("failed to create file: %w", err) } + defer file.Close() - // Save to file - if err := os.WriteFile(filename, fitData, 0644); err != nil { - return fmt.Errorf("failed to save FIT file %s: %w", filename, err) + // Download FIT file + if err := c.client.ExportActivity(activityId, file, garminconnect.ActivityFormatFIT); err != nil { + return fmt.Errorf("failed to export activity %d: %w", activityId, err) } return nil