package stats import ( "encoding/json" "log" "net/http" "os" "sync" "sync/atomic" "time" "software-station/internal/models" "golang.org/x/time/rate" ) type Service struct { HashesFile string KnownHashes struct { sync.RWMutex Data map[string]*models.FingerprintData } GlobalStats struct { sync.RWMutex UniqueRequests map[string]bool SuccessDownloads map[string]bool BlockedRequests map[string]bool LimitedRequests map[string]bool WebRequests map[string]bool TotalResponseTime time.Duration TotalRequests int64 TotalBytes int64 StartTime time.Time } DownloadStats struct { sync.RWMutex Limiters map[string]*rate.Limiter } hashesDirty int32 stopChan chan struct{} } func NewService(hashesFile string) *Service { s := &Service{ HashesFile: hashesFile, stopChan: make(chan struct{}), } s.KnownHashes.Data = make(map[string]*models.FingerprintData) s.GlobalStats.UniqueRequests = make(map[string]bool) s.GlobalStats.SuccessDownloads = make(map[string]bool) s.GlobalStats.BlockedRequests = make(map[string]bool) s.GlobalStats.LimitedRequests = make(map[string]bool) s.GlobalStats.WebRequests = make(map[string]bool) s.GlobalStats.StartTime = time.Now() s.DownloadStats.Limiters = make(map[string]*rate.Limiter) return s } func (s *Service) Start() { go func() { ticker := time.NewTicker(10 * time.Second) statsTicker := time.NewTicker(24 * time.Hour) defer ticker.Stop() defer statsTicker.Stop() for { select { case <-ticker.C: if atomic.CompareAndSwapInt32(&s.hashesDirty, 1, 0) { s.FlushHashes() } case <-statsTicker.C: s.ResetGlobalStats() case <-s.stopChan: s.FlushHashes() return } } }() } func (s *Service) ResetGlobalStats() { s.GlobalStats.Lock() defer s.GlobalStats.Unlock() s.GlobalStats.UniqueRequests = make(map[string]bool) s.GlobalStats.SuccessDownloads = make(map[string]bool) s.GlobalStats.BlockedRequests = make(map[string]bool) s.GlobalStats.LimitedRequests = make(map[string]bool) s.GlobalStats.WebRequests = make(map[string]bool) s.GlobalStats.TotalRequests = 0 s.GlobalStats.TotalResponseTime = 0 s.GlobalStats.StartTime = time.Now() } func (s *Service) Stop() { close(s.stopChan) } func (s *Service) LoadHashes() { data, err := os.ReadFile(s.HashesFile) if err != nil { if !os.IsNotExist(err) { log.Printf("Error reading hashes file: %v", err) } return } s.KnownHashes.Lock() defer s.KnownHashes.Unlock() if err := json.Unmarshal(data, &s.KnownHashes.Data); err != nil { log.Printf("Error unmarshaling hashes: %v", err) return } var total int64 for _, d := range s.KnownHashes.Data { total += atomic.LoadInt64(&d.TotalBytes) } atomic.StoreInt64(&s.GlobalStats.TotalBytes, total) } func (s *Service) SaveHashes() { atomic.StoreInt32(&s.hashesDirty, 1) } func (s *Service) FlushHashes() { s.KnownHashes.RLock() data, err := json.MarshalIndent(s.KnownHashes.Data, "", " ") s.KnownHashes.RUnlock() if err != nil { log.Printf("Error marshaling hashes: %v", err) return } if err := os.WriteFile(s.HashesFile, data, 0600); err != nil { log.Printf("Error writing hashes file: %v", err) } } func (s *Service) APIStatsHandler(w http.ResponseWriter, r *http.Request) { s.GlobalStats.RLock() defer s.GlobalStats.RUnlock() avgResponse := time.Duration(0) if s.GlobalStats.TotalRequests > 0 { avgResponse = s.GlobalStats.TotalResponseTime / time.Duration(s.GlobalStats.TotalRequests) } totalBytes := atomic.LoadInt64(&s.GlobalStats.TotalBytes) uptime := time.Since(s.GlobalStats.StartTime) avgSpeed := float64(totalBytes) / uptime.Seconds() status := "healthy" if s.GlobalStats.TotalRequests > 0 && float64(len(s.GlobalStats.BlockedRequests))/float64(s.GlobalStats.TotalRequests) > 0.5 { status = "unhealthy" } data := map[string]interface{}{ "total_unique_download_requests": len(s.GlobalStats.UniqueRequests), "total_unique_success_downloads": len(s.GlobalStats.SuccessDownloads), "total_unique_blocked": len(s.GlobalStats.BlockedRequests), "total_unique_limited": len(s.GlobalStats.LimitedRequests), "total_unique_web_requests": len(s.GlobalStats.WebRequests), "avg_speed_bps": avgSpeed, "avg_response_time": avgResponse.String(), "uptime": uptime.String(), "status": status, } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(data); err != nil { log.Printf("Error encoding stats: %v", err) } }