feat: sync project
This commit is contained in:
128
mengyastore-backend/internal/storage/sitestore.go
Normal file
128
mengyastore-backend/internal/storage/sitestore.go
Normal file
@@ -0,0 +1,128 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user