package state import ( "encoding/json" "fmt" "os" "path/filepath" "sync" "time" ) // ActivityInfo holds information about a downloaded activity type ActivityInfo struct { ID int64 `json:"id"` Name string `json:"name"` Date time.Time `json:"date"` FilePath string `json:"file_path"` DownloadedAt time.Time `json:"downloaded_at"` SportType string `json:"sport_type,omitempty"` } // State manages the persistent state of downloaded activities type State struct { DownloadedActivities map[int64]ActivityInfo `json:"downloaded_activities"` LastCheck time.Time `json:"last_check"` TotalDownloaded int `json:"total_downloaded"` filePath string mu sync.RWMutex } // New creates a new State instance func New(filePath string) *State { return &State{ DownloadedActivities: make(map[int64]ActivityInfo), LastCheck: time.Time{}, TotalDownloaded: 0, filePath: filePath, } } // Load loads state from JSON file func (s *State) Load() error { s.mu.Lock() defer s.mu.Unlock() // Check if file exists if _, err := os.Stat(s.filePath); os.IsNotExist(err) { // File doesn't exist, start with empty state return nil } // Read file data, err := os.ReadFile(s.filePath) if err != nil { return fmt.Errorf("failed to read state file: %w", err) } // Unmarshal JSON if err := json.Unmarshal(data, s); err != nil { // Try to backup corrupted file backupPath := s.filePath + ".backup" _ = os.WriteFile(backupPath, data, 0600) return fmt.Errorf("failed to unmarshal state file (backed up to %s): %w", backupPath, err) } return nil } // Save saves state to JSON file func (s *State) Save() error { s.mu.RLock() defer s.mu.RUnlock() // Create directory if it doesn't exist dir := filepath.Dir(s.filePath) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("failed to create state directory: %w", err) } // Marshal to JSON data, err := json.MarshalIndent(s, "", " ") if err != nil { return fmt.Errorf("failed to marshal state: %w", err) } // Write to temporary file first (atomic write) tmpFile := s.filePath + ".tmp" if err := os.WriteFile(tmpFile, data, 0600); err != nil { return fmt.Errorf("failed to write temporary state file: %w", err) } // Rename temporary file to actual file (atomic operation) if err := os.Rename(tmpFile, s.filePath); err != nil { return fmt.Errorf("failed to rename temporary state file: %w", err) } return nil } // IsDownloaded checks if an activity has already been downloaded func (s *State) IsDownloaded(activityID int64) bool { s.mu.RLock() defer s.mu.RUnlock() _, exists := s.DownloadedActivities[activityID] return exists } // MarkDownloaded marks an activity as downloaded func (s *State) MarkDownloaded(info ActivityInfo) error { s.mu.Lock() defer s.mu.Unlock() s.DownloadedActivities[info.ID] = info s.TotalDownloaded++ s.LastCheck = time.Now() // Save state after marking downloaded return s.save() } // UpdateLastCheck updates the last check timestamp func (s *State) UpdateLastCheck() error { s.mu.Lock() defer s.mu.Unlock() s.LastCheck = time.Now() return s.save() } // GetStats returns statistics about downloaded activities func (s *State) GetStats() (totalDownloaded int, lastCheck time.Time) { s.mu.RLock() defer s.mu.RUnlock() return s.TotalDownloaded, s.LastCheck } // save is an internal method that saves without locking (assumes lock is held) func (s *State) save() error { // Create directory if it doesn't exist dir := filepath.Dir(s.filePath) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("failed to create state directory: %w", err) } // Marshal to JSON data, err := json.MarshalIndent(s, "", " ") if err != nil { return fmt.Errorf("failed to marshal state: %w", err) } // Write to temporary file first (atomic write) tmpFile := s.filePath + ".tmp" if err := os.WriteFile(tmpFile, data, 0600); err != nil { return fmt.Errorf("failed to write temporary state file: %w", err) } // Rename temporary file to actual file (atomic operation) if err := os.Rename(tmpFile, s.filePath); err != nil { return fmt.Errorf("failed to rename temporary state file: %w", err) } return nil }