aboutsummaryrefslogtreecommitdiff
path: root/backend/stats.go
diff options
context:
space:
mode:
authorAnhgelus Morhtuuzh <william@herges.fr>2025-12-22 18:34:02 +0100
committerAnhgelus Morhtuuzh <william@herges.fr>2025-12-22 18:34:02 +0100
commitcbd5c09c5e1403709d4aabf91051443f147689e5 (patch)
tree5add0c1a15df56ec121a77f3d4bd895c7d5f19df /backend/stats.go
parent9bba6dcbb2e83fe53604d38b89fb79ce47eacddd (diff)
refactor(backend): move db related in new package
Diffstat (limited to 'backend/stats.go')
-rw-r--r--backend/stats.go226
1 files changed, 0 insertions, 226 deletions
diff --git a/backend/stats.go b/backend/stats.go
deleted file mode 100644
index f081194..0000000
--- a/backend/stats.go
+++ /dev/null
@@ -1,226 +0,0 @@
-package backend
-
-import (
- "context"
- "database/sql"
- "fmt"
- "log/slog"
- "net/http"
- "regexp"
- "slices"
- "strconv"
- "strings"
- "sync"
- "time"
-
- "github.com/go-chi/chi/v5"
-)
-
-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) 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 == ctx.Value(configKey).(*Config).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 statRow struct {
- Origin string
- Target string
- Visit uint
-}
-
-const statPerPage = 25
-
-func GetStatRows(ctx context.Context, page uint) ([]statRow, error) {
- rows, err := getDB(ctx).QueryContext(
- ctx,
- "SELECT origin, target, visit FROM stats ORDER BY visit DESC LIMIT ? OFFSET ?",
- statPerPage, (page-1)*statPerPage,
- )
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- statRows := make([]statRow, statPerPage)
- var i uint8
- for i = 0; rows.Next(); i++ {
- var stat statRow
- 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 GetUnionStatRows(ctx context.Context) ([]statRow, 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 statRow
- 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 []statRow
- for k, v := range data {
- statRows = append(statRows, statRow{
- Target: k,
- Visit: v,
- })
- }
- slices.SortFunc(statRows, func(a, b statRow) int {
- return int(b.Visit) - int(a.Visit)
- })
- return statRows, nil
-}
-
-type adminData struct {
- *data
- Visits []statRow
- Rows []statRow
- PagesNumber int
- CurrentPage int
-}
-
-func HandleAdmin(r *chi.Mux) {
- r.Get("/admin", func(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- if !ctx.Value(loginKey).(bool) {
- w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
- http.Error(w, "Unauthorized", http.StatusUnauthorized)
- return
- }
- d := new(adminData)
- d.data = new(data)
- rawPage := r.URL.Query().Get("page")
- page := 1
- var err error
- if rawPage != "" {
- page, err = strconv.Atoi(rawPage)
- if err != nil || page < 1 {
- slog.Warn("invalid page number", "rawPage", rawPage)
- http.Error(w, "Bad request", http.StatusBadRequest)
- return
- }
- }
- d.Rows, err = GetStatRows(ctx, uint(page))
- if err != nil {
- panic(err)
- }
- d.Visits, err = GetUnionStatRows(ctx)
- if err != nil {
- panic(err)
- }
- d.PagesNumber = page + max(len(d.Rows)-statPerPage+1, 0)
- d.CurrentPage = page
- d.handleGeneric(w, r, "admin", d)
- })
-}