diff options
| author | Anhgelus Morhtuuzh <william@herges.fr> | 2025-12-22 18:34:02 +0100 |
|---|---|---|
| committer | Anhgelus Morhtuuzh <william@herges.fr> | 2025-12-22 18:34:02 +0100 |
| commit | cbd5c09c5e1403709d4aabf91051443f147689e5 (patch) | |
| tree | 5add0c1a15df56ec121a77f3d4bd895c7d5f19df /backend/storage/stats.go | |
| parent | 9bba6dcbb2e83fe53604d38b89fb79ce47eacddd (diff) | |
refactor(backend): move db related in new package
Diffstat (limited to 'backend/storage/stats.go')
| -rw-r--r-- | backend/storage/stats.go | 182 |
1 files changed, 182 insertions, 0 deletions
diff --git a/backend/storage/stats.go b/backend/storage/stats.go new file mode 100644 index 0000000..757168d --- /dev/null +++ b/backend/storage/stats.go @@ -0,0 +1,182 @@ +package storage + +import ( + "context" + "database/sql" + "fmt" + "log/slog" + "net/http" + "regexp" + "slices" + "strings" + "sync" + "time" +) + +const DBKey = "db" + +type loaded struct { + data map[string]struct{} + mu *sync.RWMutex +} + +func (l *loaded) Has(k string) bool { + l.mu.RLock() + defer l.mu.RUnlock() + _, ok := l.data[k] + return ok +} + +func (l *loaded) Add(k string) { + l.mu.Lock() + defer l.mu.Unlock() + l.data[k] = struct{}{} +} + +func (l *loaded) Remove(k string) { + l.mu.Lock() + defer l.mu.Unlock() + delete(l.data, k) +} + +func newLoaded() *loaded { + return &loaded{ + data: make(map[string]struct{}), + mu: new(sync.RWMutex), + } +} + +var trimRefererReg = regexp.MustCompile(`https?://([a-z-0-9.]+(:\d+)?)/.*`) + +var load = newLoaded() + +func getDB(ctx context.Context) *sql.DB { + return ctx.Value(DBKey).(*sql.DB) +} + +func UpdateStats(ctx context.Context, r *http.Request, domain string) error { + target := r.URL.Path + if strings.HasPrefix(target, "/static") || strings.HasPrefix(target, "/admin") { + return nil + } + ref := r.Header.Get("Referer") + if ref == "" { + return nil + } + subs := trimRefererReg.FindStringSubmatch(ref) + if len(subs) < 2 { + return nil + } + ref = subs[1] + if ref == domain || ref == fmt.Sprintf("localhost:%d", 8000) { + ref = subs[0][strings.Index(subs[0], ref)+len(ref):] + if ref == target || strings.HasPrefix(ref, "/admin") || ref == "/favicon.ico" { + return nil + } + } + // using /assets/styles.css to detect if a page is loaded → majority of bots will not load this + if target == "/assets/styles.css" { + target = ref + ref = "?" + } + if load.Has(target) { + return nil + } + db := getDB(ctx) + rows, err := db.QueryContext(ctx, "SELECT id, visit FROM stats WHERE origin = ? AND target = ?", ref, target) + if err != nil { + return err + } + defer func() { + if err == nil { + slog.Debug("stats updated") + load.Add(target) + go func(target string) { + time.Sleep(5 * time.Second) + load.Remove(target) + }(target) + } + }() + if !rows.Next() { + _, err = db.ExecContext(ctx, "INSERT INTO stats (origin, target, visit) VALUES (?, ?, 1)", ref, target) + return err + } + var id uint + var nb uint + err = rows.Scan(&id, &nb) + if err != nil { + return err + } + err = rows.Close() + if err != nil { + return err + } + _, err = db.ExecContext(ctx, "UPDATE stats SET visit = ? WHERE id = ?", nb+1, id) + return err +} + +type StatsRow struct { + Origin string + Target string + Visit uint +} + +const StatsPerPage = 25 + +func GetStatsRows(ctx context.Context, page uint) ([]StatsRow, error) { + rows, err := getDB(ctx).QueryContext( + ctx, + "SELECT origin, target, visit FROM stats ORDER BY visit DESC LIMIT ? OFFSET ?", + StatsPerPage, (page-1)*StatsPerPage, + ) + if err != nil { + return nil, err + } + defer rows.Close() + statRows := make([]StatsRow, StatsPerPage) + var i uint8 + for i = 0; rows.Next(); i++ { + var stat StatsRow + err = rows.Scan(&stat.Origin, &stat.Target, &stat.Visit) + if err != nil { + return nil, err + } + statRows[i] = stat + } + if i == 0 { + return nil, nil + } + return statRows[:i], nil +} + +func GetUnionStatsRows(ctx context.Context) ([]StatsRow, error) { + rows, err := getDB(ctx).QueryContext(ctx, "SELECT target, visit FROM stats ORDER BY visit DESC") + if err != nil { + return nil, err + } + defer rows.Close() + data := make(map[string]uint) + for rows.Next() { + var stat StatsRow + err = rows.Scan(&stat.Target, &stat.Visit) + if err != nil { + return nil, err + } + if _, ok := data[stat.Target]; !ok { + data[stat.Target] = stat.Visit + } else { + data[stat.Target] += stat.Visit + } + } + var statRows []StatsRow + for k, v := range data { + statRows = append(statRows, StatsRow{ + Target: k, + Visit: v, + }) + } + slices.SortFunc(statRows, func(a, b StatsRow) int { + return int(b.Visit) - int(a.Visit) + }) + return statRows, nil +} |
