package avatar import ( "errors" "net/http" "github.com/gin-gonic/gin" "galo/internal/auth" "galo/internal/httpx" "galo/internal/photos" ) // avatarCacheControl is safe to cache aggressively because the URL carries an // opaque per-generation version segment: replacing the avatar changes the URL. const avatarCacheControl = "private, max-age=31536000, immutable" type Handler struct { svc *Service } func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } // Upload installs or replaces the authenticated user's avatar from a multipart // "file" field and returns the updated self profile. func (h *Handler) Upload(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, h.svc.MaxUploadBytes()) fileHeader, err := c.FormFile("file") if err != nil { var maxErr *http.MaxBytesError if errors.As(err, &maxErr) { httpx.RespondError(c, httpx.NewError(http.StatusRequestEntityTooLarge, "file_too_large", "avatar exceeds the maximum upload size")) return } httpx.RespondError(c, httpx.BadRequest("invalid_request", "file is required")) return } f, err := fileHeader.Open() if err != nil { httpx.RespondError(c, err) return } defer f.Close() u, err := h.svc.SetAvatar(c.Request.Context(), auth.UserID(c), f) if err != nil { httpx.RespondError(c, err) return } c.JSON(http.StatusOK, gin.H{"user": u.Self()}) } // Remove resets the avatar to the default placeholder and returns the updated // self profile. func (h *Handler) Remove(c *gin.Context) { u, err := h.svc.RemoveAvatar(c.Request.Context(), auth.UserID(c)) if err != nil { httpx.RespondError(c, err) return } c.JSON(http.StatusOK, gin.H{"user": u.Self()}) } // Serve streams the user's avatar. The version segment only busts caches; the // current generation is always served. func (h *Handler) Serve(c *gin.Context) { large := c.Query("size") == "large" data, err := h.svc.AvatarImage(c.Request.Context(), c.Param("userID"), large) if err != nil { if errors.Is(err, photos.ErrObjectNotFound) { httpx.RespondError(c, httpx.NotFound("avatar_not_found", "avatar not found")) return } httpx.RespondError(c, err) return } c.Header("Cache-Control", avatarCacheControl) c.Data(http.StatusOK, "image/webp", data) }