package fbhttp import ( "context" "fmt" "os" "time" "github.com/jellydator/ttlcache/v3" ) const uploadCacheTTL = 3 * time.Minute // UploadCache is an interface for tracking active uploads. // Allows for different backends (e.g. in-memory or redis) // to support both single instance and multi replica deployments. type UploadCache interface { // Register stores an upload with its expected file size Register(filePath string, fileSize int64) // Complete removes an upload from the cache Complete(filePath string) // GetLength returns the expected file size for an active upload GetLength(filePath string) (int64, error) // Touch refreshes the TTL for an active upload Touch(filePath string) // Close cleans up any resources Close() } // memoryUploadCache is an upload cache for single replica deployments type memoryUploadCache struct { cache *ttlcache.Cache[string, int64] } func newMemoryUploadCache() *memoryUploadCache { cache := ttlcache.New[string, int64]() cache.OnEviction(func(_ context.Context, reason ttlcache.EvictionReason, item *ttlcache.Item[string, int64]) { if reason == ttlcache.EvictionReasonExpired { fmt.Printf("deleting incomplete upload file: \"%s\"\n", item.Key()) os.Remove(item.Key()) } }) go cache.Start() return &memoryUploadCache{cache: cache} } func (c *memoryUploadCache) Register(filePath string, fileSize int64) { c.cache.Set(filePath, fileSize, uploadCacheTTL) } func (c *memoryUploadCache) Complete(filePath string) { c.cache.Delete(filePath) } func (c *memoryUploadCache) GetLength(filePath string) (int64, error) { item := c.cache.Get(filePath) if item == nil { return 0, fmt.Errorf("no active upload found for the given path") } return item.Value(), nil } func (c *memoryUploadCache) Touch(filePath string) { c.cache.Touch(filePath) } func (c *memoryUploadCache) Close() { c.cache.Stop() } // NewUploadCache creates a new upload cache. // If redisURL is empty, an in-memory cache will be used (suitable for single instance deployments). // Otherwise, Redis will be used for the cache (suitable for multi-instance deployments). // The redisURL can include credentials, e.g. redis://user:pass@host:port func NewUploadCache(redisURL string) (UploadCache, error) { if redisURL != "" { return newRedisUploadCache(redisURL) } return newMemoryUploadCache(), nil }