166
internal/state/state.go
Normal file
166
internal/state/state.go
Normal file
@@ -0,0 +1,166 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user