129 lines
2.8 KiB
Go
129 lines
2.8 KiB
Go
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
|
|
}
|