A community based topic aggregation platform built on atproto
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}