Files
Artifacto/internal/server/ratelimit.go
dtoro c7d7ee287c initial commit: Artifacto v0.1
Self-hosted HTML artifact publisher. Single Go binary, SQLite metadata,
HTML on disk. Features: password-protected artifacts, per-artifact expiration,
admin dashboard with view metrics (privacy-preserving daily-salt visitor hash,
sparklines, 30-day SVG chart, top referrers), rate-limited unlock endpoint.

Packaged as a Docker image (two-stage, CGO_ENABLED=0, pure-Go SQLite).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:45:57 +02:00

56 lines
1.1 KiB
Go

package server
import (
"sync"
"time"
)
// Simple token bucket keyed by arbitrary string. Thread-safe.
type bucket struct {
tokens float64
lastRef time.Time
}
type RateLimiter struct {
mu sync.Mutex
buckets map[string]*bucket
capacity float64
refill float64 // tokens per second
}
// NewRateLimiter: e.g. capacity=5, per=10min → 5 tokens, refilled at 5/600 tokens/sec.
func NewRateLimiter(capacity int, per time.Duration) *RateLimiter {
return &RateLimiter{
buckets: map[string]*bucket{},
capacity: float64(capacity),
refill: float64(capacity) / per.Seconds(),
}
}
// Allow returns true if the key has a token to spend.
func (l *RateLimiter) Allow(key string) bool {
l.mu.Lock()
defer l.mu.Unlock()
b, ok := l.buckets[key]
now := time.Now()
if !ok {
b = &bucket{tokens: l.capacity, lastRef: now}
l.buckets[key] = b
}
elapsed := now.Sub(b.lastRef).Seconds()
b.tokens = minF(l.capacity, b.tokens+elapsed*l.refill)
b.lastRef = now
if b.tokens >= 1 {
b.tokens--
return true
}
return false
}
func minF(a, b float64) float64 {
if a < b {
return a
}
return b
}