Files
Thomas Klaehn f496eebe2a Initial commit
Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
2026-02-10 13:27:42 +01:00

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
}