2026-05-31 22:27:43 +03:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// uploadGroupWindow is how long after a batched upload a follow-up upload with
|
|
|
|
|
// the same X-Warpbox-Batch value (and same account/IP) is folded into the same
|
|
|
|
|
// box. ShareX sends a multi-file selection as separate back-to-back requests;
|
|
|
|
|
// the batch header lets it land them in one box.
|
|
|
|
|
const uploadGroupWindow = 20 * time.Second
|
|
|
|
|
|
|
|
|
|
// uploadBatchHeader is the opt-in request header. Without it, uploads behave
|
|
|
|
|
// exactly as before (one box per request). With it, requests sharing the same
|
|
|
|
|
// value (per account/IP) within uploadGroupWindow are grouped into one box.
|
|
|
|
|
const uploadBatchHeader = "X-Warpbox-Batch"
|
|
|
|
|
|
2026-06-01 00:12:43 +03:00
|
|
|
// uploadGroupPruneInterval is how often entryFor drops stale entries so the map
|
|
|
|
|
// can't grow without bound (one key per account/IP + batch value otherwise).
|
|
|
|
|
const uploadGroupPruneInterval = 5 * time.Minute
|
|
|
|
|
|
2026-05-31 22:27:43 +03:00
|
|
|
// uploadGrouper tracks the most recent box per batch key so opt-in batched
|
|
|
|
|
// uploads land in a single box. Each key has its own lock, which also serialises
|
|
|
|
|
// that key's concurrent uploads so they append to the same box instead of racing
|
|
|
|
|
// to create several.
|
|
|
|
|
type uploadGrouper struct {
|
2026-06-01 00:12:43 +03:00
|
|
|
mu sync.Mutex
|
|
|
|
|
entries map[string]*uploadGroupEntry
|
|
|
|
|
lastPrune time.Time
|
2026-05-31 22:27:43 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type uploadGroupEntry struct {
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
boxID string
|
|
|
|
|
manageURL string
|
|
|
|
|
deleteURL string
|
|
|
|
|
at time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newUploadGrouper() *uploadGrouper {
|
|
|
|
|
return &uploadGrouper{entries: make(map[string]*uploadGroupEntry)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g *uploadGrouper) entryFor(key string) *uploadGroupEntry {
|
|
|
|
|
g.mu.Lock()
|
|
|
|
|
defer g.mu.Unlock()
|
2026-06-01 00:12:43 +03:00
|
|
|
g.pruneLocked(time.Now())
|
2026-05-31 22:27:43 +03:00
|
|
|
entry, ok := g.entries[key]
|
|
|
|
|
if !ok {
|
|
|
|
|
entry = &uploadGroupEntry{}
|
|
|
|
|
g.entries[key] = entry
|
|
|
|
|
}
|
|
|
|
|
return entry
|
|
|
|
|
}
|
2026-06-01 00:12:43 +03:00
|
|
|
|
|
|
|
|
// pruneLocked drops entries whose last use is well past the grouping window so
|
|
|
|
|
// the map stays bounded to recently-active keys. Callers must hold g.mu. Entries
|
|
|
|
|
// currently in use, or freshly created but not yet used (zero timestamp), are
|
|
|
|
|
// kept to avoid removing one a request is about to populate.
|
|
|
|
|
func (g *uploadGrouper) pruneLocked(now time.Time) {
|
|
|
|
|
if now.Sub(g.lastPrune) < uploadGroupPruneInterval {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
g.lastPrune = now
|
|
|
|
|
for key, entry := range g.entries {
|
|
|
|
|
if !entry.mu.TryLock() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
stale := !entry.at.IsZero() && now.Sub(entry.at) > 2*uploadGroupWindow
|
|
|
|
|
entry.mu.Unlock()
|
|
|
|
|
if stale {
|
|
|
|
|
delete(g.entries, key)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|