// Package presigncache provides a Valkey (Redis-compatible) backed cache for // MinIO presigned URLs. The backend generates presigned URLs and stores them // here with a TTL; subsequent requests for the same key return the cached URL // without re-contacting MinIO. // // Design: // - Cache is intentionally best-effort: Get returns ("", false, nil) on any // Valkey error, so callers always have a fallback path to regenerate. // - Set silently drops errors — a miss on the next request is acceptable. // - TTL should be set shorter than the actual presigned URL lifetime so that // cached URLs are always valid when served. Recommended: 55 minutes for a // 1-hour presigned URL. package presigncache import ( "context" "fmt" "time" "github.com/redis/go-redis/v9" ) // Cache is the interface for presign URL caching. // Implementations must be safe for concurrent use. type Cache interface { // Get returns the cached URL for key. ok is false on cache miss or error. Get(ctx context.Context, key string) (url string, ok bool, err error) // Set stores url under key with the given TTL. Set(ctx context.Context, key, url string, ttl time.Duration) error // Delete removes key from the cache. Delete(ctx context.Context, key string) error } // ValkeyCache is a Cache backed by Valkey / Redis via go-redis. type ValkeyCache struct { rdb *redis.Client } // New creates a ValkeyCache connecting to addr (e.g. "valkey:6379"). // The connection is not established until the first command; use Ping to // verify connectivity at startup. func New(addr string) *ValkeyCache { rdb := redis.NewClient(&redis.Options{ Addr: addr, DialTimeout: 2 * time.Second, ReadTimeout: 1 * time.Second, WriteTimeout: 1 * time.Second, }) return &ValkeyCache{rdb: rdb} } // Ping checks connectivity. Call once at startup. func (c *ValkeyCache) Ping(ctx context.Context) error { if err := c.rdb.Ping(ctx).Err(); err != nil { return fmt.Errorf("presigncache: ping valkey: %w", err) } return nil } // Get returns (url, true, nil) on hit, ("", false, nil) on miss, and // ("", false, err) only on unexpected errors (not redis.Nil). func (c *ValkeyCache) Get(ctx context.Context, key string) (string, bool, error) { val, err := c.rdb.Get(ctx, key).Result() if err == redis.Nil { return "", false, nil } if err != nil { return "", false, fmt.Errorf("presigncache: get %q: %w", key, err) } return val, true, nil } // Set stores url under key with ttl. Errors are returned but are non-fatal // for callers — a Set failure means the next request will miss and regenerate. func (c *ValkeyCache) Set(ctx context.Context, key, url string, ttl time.Duration) error { if err := c.rdb.Set(ctx, key, url, ttl).Err(); err != nil { return fmt.Errorf("presigncache: set %q: %w", key, err) } return nil } // Delete removes key from the cache. It is not an error if the key does not exist. func (c *ValkeyCache) Delete(ctx context.Context, key string) error { if err := c.rdb.Del(ctx, key).Err(); err != nil { return fmt.Errorf("presigncache: delete %q: %w", key, err) } return nil } // NoopCache is a no-op Cache that always returns a miss. Used when Valkey is // not configured (e.g. local development without Docker). type NoopCache struct{} func (NoopCache) Get(_ context.Context, _ string) (string, bool, error) { return "", false, nil } func (NoopCache) Set(_ context.Context, _, _ string, _ time.Duration) error { return nil } func (NoopCache) Delete(_ context.Context, _ string) error { return nil }