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/stats.go | |
| parent | 9bba6dcbb2e83fe53604d38b89fb79ce47eacddd (diff) | |
refactor(backend): move db related in new package
Diffstat (limited to 'backend/stats.go')
| -rw-r--r-- | backend/stats.go | 226 |
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) - }) -} |
