// Package handlers provides HTTP request handlers. package handlers import ( "context" "encoding/json" "log/slog" "net/http" "time" "go.uber.org/fx" "sneak.berlin/go/pixa/internal/config" "sneak.berlin/go/pixa/internal/database" "sneak.berlin/go/pixa/internal/encurl" "sneak.berlin/go/pixa/internal/healthcheck" "sneak.berlin/go/pixa/internal/imgcache" "sneak.berlin/go/pixa/internal/logger" "sneak.berlin/go/pixa/internal/session" ) // Params defines dependencies for Handlers. type Params struct { fx.In Logger *logger.Logger Healthcheck *healthcheck.Healthcheck Database *database.Database Config *config.Config } // Handlers provides HTTP request handlers. type Handlers struct { log *slog.Logger hc *healthcheck.Healthcheck db *database.Database config *config.Config imgSvc *imgcache.Service imgCache *imgcache.Cache sessMgr *session.Manager encGen *encurl.Generator } // New creates a new Handlers instance. func New(lc fx.Lifecycle, params Params) (*Handlers, error) { s := &Handlers{ log: params.Logger.Get(), hc: params.Healthcheck, db: params.Database, config: params.Config, } lc.Append(fx.Hook{ OnStart: func(_ context.Context) error { return s.initImageService() }, }) return s, nil } // initImageService initializes the image cache and service. func (s *Handlers) initImageService() error { // Create the cache cache, err := imgcache.NewCache(s.db.DB(), imgcache.CacheConfig{ StateDir: s.config.StateDir, CacheTTL: imgcache.DefaultCacheTTL, NegativeTTL: imgcache.DefaultNegativeTTL, HotCacheSize: imgcache.DefaultHotCacheSize, HotCacheEnabled: true, }) if err != nil { return err } s.imgCache = cache // Create the fetcher config fetcherCfg := imgcache.DefaultFetcherConfig() fetcherCfg.AllowHTTP = s.config.AllowHTTP if s.config.UpstreamConnectionsPerHost > 0 { fetcherCfg.MaxConnectionsPerHost = s.config.UpstreamConnectionsPerHost } // Create the service svc, err := imgcache.NewService(&imgcache.ServiceConfig{ Cache: cache, FetcherConfig: fetcherCfg, SigningKey: s.config.SigningKey, Whitelist: s.config.WhitelistHosts, Logger: s.log, }) if err != nil { return err } s.imgSvc = svc s.log.Info("image service initialized") // Initialize session manager and URL generator if signing key is configured if s.config.SigningKey != "" { sessMgr, err := session.NewManager(s.config.SigningKey, !s.config.Debug) if err != nil { return err } s.sessMgr = sessMgr encGen, err := encurl.NewGenerator(s.config.SigningKey) if err != nil { return err } s.encGen = encGen s.log.Info("session manager and URL generator initialized") } return nil } func (s *Handlers) respondJSON(w http.ResponseWriter, data interface{}, status int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if data != nil { err := json.NewEncoder(w).Encode(data) if err != nil { s.log.Error("json encode error", "error", err) } } } func (s *Handlers) respondError(w http.ResponseWriter, message string, status int) { s.respondJSON(w, map[string]interface{}{ "error": message, "status": status, "timestamp": time.Now().UTC().Format(time.RFC3339), }, status) }