167 lines
4.2 KiB
Go
167 lines
4.2 KiB
Go
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
|
|
}
|