A community based topic aggregation platform built on atproto
at main 232 lines 7.4 kB view raw
1package imageproxy 2 3import ( 4 "errors" 5 "fmt" 6 "log/slog" 7 "os" 8 "strconv" 9 "time" 10) 11 12// Config validation errors 13var ( 14 // ErrInvalidCacheMaxGB is returned when CacheMaxGB is not positive 15 ErrInvalidCacheMaxGB = errors.New("CacheMaxGB must be positive") 16 // ErrInvalidFetchTimeout is returned when FetchTimeout is not positive 17 ErrInvalidFetchTimeout = errors.New("FetchTimeout must be positive") 18 // ErrInvalidMaxSourceSize is returned when MaxSourceSizeMB is not positive 19 ErrInvalidMaxSourceSize = errors.New("MaxSourceSizeMB must be positive") 20 // ErrMissingCachePath is returned when CachePath is empty while Enabled is true 21 ErrMissingCachePath = errors.New("CachePath is required when proxy is enabled") 22 // ErrInvalidCacheTTL is returned when CacheTTLDays is negative 23 ErrInvalidCacheTTL = errors.New("CacheTTLDays cannot be negative") 24) 25 26// Config holds the configuration for the image proxy service. 27type Config struct { 28 // Enabled determines whether the image proxy service is active. 29 Enabled bool 30 31 // BaseURL is the origin/domain for the image proxy service (e.g., "https://coves.social"). 32 // Empty string generates relative URLs (e.g., "/img/avatar/plain/did/cid"). 33 // The /img path prefix is added automatically by the URL generation function. 34 BaseURL string 35 36 // CachePath is the filesystem path where cached images are stored. 37 CachePath string 38 39 // CacheMaxGB is the maximum cache size in gigabytes. 40 CacheMaxGB int 41 42 // CacheTTLDays is the maximum age in days for cached entries. 43 // Entries older than this are eligible for cleanup regardless of cache size. 44 // Set to 0 to disable TTL-based cleanup (only LRU eviction applies). 45 CacheTTLDays int 46 47 // CleanupInterval is how often to run cache cleanup (TTL + LRU eviction). 48 // Set to 0 to disable background cleanup. 49 CleanupInterval time.Duration 50 51 // CDNURL is the optional CDN URL prefix for serving cached images. 52 CDNURL string 53 54 // FetchTimeout is the maximum time allowed for fetching images from PDS. 55 FetchTimeout time.Duration 56 57 // MaxSourceSizeMB is the maximum allowed size for source images in megabytes. 58 MaxSourceSizeMB int 59} 60 61// NewConfig creates a new Config with the provided values and validates it. 62// This is the recommended way to create a Config, as it ensures all invariants are satisfied. 63// Use DefaultConfig() or ConfigFromEnv() for convenient config creation with sensible defaults. 64func NewConfig( 65 enabled bool, 66 baseURL string, 67 cachePath string, 68 cacheMaxGB int, 69 cacheTTLDays int, 70 cleanupInterval time.Duration, 71 cdnURL string, 72 fetchTimeout time.Duration, 73 maxSourceSizeMB int, 74) (Config, error) { 75 cfg := Config{ 76 Enabled: enabled, 77 BaseURL: baseURL, 78 CachePath: cachePath, 79 CacheMaxGB: cacheMaxGB, 80 CacheTTLDays: cacheTTLDays, 81 CleanupInterval: cleanupInterval, 82 CDNURL: cdnURL, 83 FetchTimeout: fetchTimeout, 84 MaxSourceSizeMB: maxSourceSizeMB, 85 } 86 87 if err := cfg.Validate(); err != nil { 88 return Config{}, err 89 } 90 91 return cfg, nil 92} 93 94// Validate checks the configuration for invalid values. 95// Returns nil if the configuration is valid, or an error describing the problem. 96// When Enabled is false, only numeric constraints are validated (for safety). 97// When Enabled is true, all required fields must be set. 98func (c Config) Validate() error { 99 // Always validate numeric constraints regardless of enabled state 100 if c.CacheMaxGB <= 0 { 101 return fmt.Errorf("%w: got %d", ErrInvalidCacheMaxGB, c.CacheMaxGB) 102 } 103 if c.FetchTimeout <= 0 { 104 return fmt.Errorf("%w: got %v", ErrInvalidFetchTimeout, c.FetchTimeout) 105 } 106 if c.MaxSourceSizeMB <= 0 { 107 return fmt.Errorf("%w: got %d", ErrInvalidMaxSourceSize, c.MaxSourceSizeMB) 108 } 109 if c.CacheTTLDays < 0 { 110 return fmt.Errorf("%w: got %d", ErrInvalidCacheTTL, c.CacheTTLDays) 111 } 112 113 // When enabled, validate required fields 114 if c.Enabled { 115 if c.CachePath == "" { 116 return ErrMissingCachePath 117 } 118 // BaseURL can be empty for relative URLs 119 } 120 121 return nil 122} 123 124// DefaultConfig returns a Config with sensible default values. 125func DefaultConfig() Config { 126 return Config{ 127 Enabled: true, 128 BaseURL: "", 129 CachePath: "/var/cache/coves/images", 130 CacheMaxGB: 10, 131 CacheTTLDays: 30, 132 CleanupInterval: 1 * time.Hour, 133 CDNURL: "", 134 FetchTimeout: 30 * time.Second, 135 MaxSourceSizeMB: 10, 136 } 137} 138 139// ConfigFromEnv creates a Config from environment variables. 140// Uses defaults for any missing environment variables. 141// 142// Environment variables: 143// - IMAGE_PROXY_ENABLED: "true"/"1" to enable, "false"/"0" to disable (default: true) 144// - IMAGE_PROXY_BASE_URL: origin URL for image proxy (default: "" for relative URLs) 145// - IMAGE_PROXY_CACHE_PATH: filesystem cache path (default: "/var/cache/coves/images") 146// - IMAGE_PROXY_CACHE_MAX_GB: max cache size in GB (default: 10) 147// - IMAGE_PROXY_CACHE_TTL_DAYS: max age for cache entries in days, 0 to disable (default: 30) 148// - IMAGE_PROXY_CLEANUP_INTERVAL_MINUTES: cleanup job interval in minutes, 0 to disable (default: 60) 149// - IMAGE_PROXY_CDN_URL: optional CDN URL prefix (default: "") 150// - IMAGE_PROXY_FETCH_TIMEOUT_SECONDS: PDS fetch timeout in seconds (default: 30) 151// - IMAGE_PROXY_MAX_SOURCE_SIZE_MB: max source image size in MB (default: 10) 152func ConfigFromEnv() Config { 153 cfg := DefaultConfig() 154 155 if v := os.Getenv("IMAGE_PROXY_ENABLED"); v != "" { 156 cfg.Enabled = v == "true" || v == "1" 157 } 158 159 if v := os.Getenv("IMAGE_PROXY_BASE_URL"); v != "" { 160 cfg.BaseURL = v 161 } 162 163 if v := os.Getenv("IMAGE_PROXY_CACHE_PATH"); v != "" { 164 cfg.CachePath = v 165 } 166 167 if v := os.Getenv("IMAGE_PROXY_CACHE_MAX_GB"); v != "" { 168 if n, err := strconv.Atoi(v); err == nil && n > 0 { 169 cfg.CacheMaxGB = n 170 } else { 171 slog.Warn("[IMAGE-PROXY] invalid IMAGE_PROXY_CACHE_MAX_GB value, using default", 172 "value", v, 173 "default", cfg.CacheMaxGB, 174 "error", err, 175 ) 176 } 177 } 178 179 if v := os.Getenv("IMAGE_PROXY_CACHE_TTL_DAYS"); v != "" { 180 if n, err := strconv.Atoi(v); err == nil && n >= 0 { 181 cfg.CacheTTLDays = n 182 } else { 183 slog.Warn("[IMAGE-PROXY] invalid IMAGE_PROXY_CACHE_TTL_DAYS value, using default", 184 "value", v, 185 "default", cfg.CacheTTLDays, 186 "error", err, 187 ) 188 } 189 } 190 191 if v := os.Getenv("IMAGE_PROXY_CLEANUP_INTERVAL_MINUTES"); v != "" { 192 if n, err := strconv.Atoi(v); err == nil && n >= 0 { 193 cfg.CleanupInterval = time.Duration(n) * time.Minute 194 } else { 195 slog.Warn("[IMAGE-PROXY] invalid IMAGE_PROXY_CLEANUP_INTERVAL_MINUTES value, using default", 196 "value", v, 197 "default_minutes", int(cfg.CleanupInterval.Minutes()), 198 "error", err, 199 ) 200 } 201 } 202 203 if v := os.Getenv("IMAGE_PROXY_CDN_URL"); v != "" { 204 cfg.CDNURL = v 205 } 206 207 if v := os.Getenv("IMAGE_PROXY_FETCH_TIMEOUT_SECONDS"); v != "" { 208 if n, err := strconv.Atoi(v); err == nil && n > 0 { 209 cfg.FetchTimeout = time.Duration(n) * time.Second 210 } else { 211 slog.Warn("[IMAGE-PROXY] invalid IMAGE_PROXY_FETCH_TIMEOUT_SECONDS value, using default", 212 "value", v, 213 "default_seconds", int(cfg.FetchTimeout.Seconds()), 214 "error", err, 215 ) 216 } 217 } 218 219 if v := os.Getenv("IMAGE_PROXY_MAX_SOURCE_SIZE_MB"); v != "" { 220 if n, err := strconv.Atoi(v); err == nil && n > 0 { 221 cfg.MaxSourceSizeMB = n 222 } else { 223 slog.Warn("[IMAGE-PROXY] invalid IMAGE_PROXY_MAX_SOURCE_SIZE_MB value, using default", 224 "value", v, 225 "default", cfg.MaxSourceSizeMB, 226 "error", err, 227 ) 228 } 229 } 230 231 return cfg 232}