package storage import ( "crypto/sha256" "encoding/json" "fmt" "os" "path/filepath" "sync" "time" ) const visitCooldown = 6 * time.Hour type siteData struct { TotalVisits int `json:"totalVisits"` } type SiteStore struct { path string mu sync.Mutex recentVisits map[string]time.Time } func NewSiteStore(path string) (*SiteStore, error) { if err := ensureSiteFile(path); err != nil { return nil, err } return &SiteStore{ path: path, recentVisits: make(map[string]time.Time), }, nil } func (s *SiteStore) RecordVisit(fingerprint string) (int, bool, error) { s.mu.Lock() defer s.mu.Unlock() now := time.Now() s.cleanupRecentVisits(now) key := buildSiteVisitKey(fingerprint) if last, ok := s.recentVisits[key]; ok && now.Sub(last) < visitCooldown { data, err := s.read() if err != nil { return 0, false, err } return data.TotalVisits, false, nil } data, err := s.read() if err != nil { return 0, false, err } data.TotalVisits++ s.recentVisits[key] = now if err := s.write(data); err != nil { return 0, false, err } return data.TotalVisits, true, nil } func (s *SiteStore) GetTotalVisits() (int, error) { s.mu.Lock() defer s.mu.Unlock() data, err := s.read() if err != nil { return 0, err } return data.TotalVisits, nil } func (s *SiteStore) read() (siteData, error) { bytes, err := os.ReadFile(s.path) if err != nil { return siteData{}, fmt.Errorf("read site data: %w", err) } var data siteData if err := json.Unmarshal(bytes, &data); err != nil { return siteData{}, fmt.Errorf("parse site data: %w", err) } return data, nil } func (s *SiteStore) write(data siteData) error { bytes, err := json.MarshalIndent(data, "", " ") if err != nil { return fmt.Errorf("encode site data: %w", err) } if err := os.WriteFile(s.path, bytes, 0o644); err != nil { return fmt.Errorf("write site data: %w", err) } return nil } func (s *SiteStore) cleanupRecentVisits(now time.Time) { for key, last := range s.recentVisits { if now.Sub(last) >= visitCooldown { delete(s.recentVisits, key) } } } func buildSiteVisitKey(fingerprint string) string { sum := sha256.Sum256([]byte("site|" + fingerprint)) return fmt.Sprintf("%x", sum) } func ensureSiteFile(path string) error { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("mkdir data dir: %w", err) } if _, err := os.Stat(path); err == nil { return nil } else if !os.IsNotExist(err) { return fmt.Errorf("stat site file: %w", err) } initial := siteData{TotalVisits: 0} bytes, err := json.MarshalIndent(initial, "", " ") if err != nil { return fmt.Errorf("init site json: %w", err) } if err := os.WriteFile(path, bytes, 0o644); err != nil { return fmt.Errorf("write site json: %w", err) } return nil }