package api import ( "bytes" "fmt" "image" "image/jpeg" "io" "net/http" "strconv" "time" "golang.org/x/image/draw" ) const ( avatarMaxDimension = 128 avatarQuality = 85 ) func (a *API) HandleUploadAvatar(w http.ResponseWriter, r *http.Request) { user := UserFromContext(r.Context()) if user == nil { writeError(w, http.StatusUnauthorized, "unauthorized") return } if err := r.ParseMultipartForm(8 << 20); err != nil { writeError(w, http.StatusBadRequest, "invalid multipart form") return } file, _, err := r.FormFile("avatar") if err != nil { writeError(w, http.StatusBadRequest, "avatar field is required") return } defer file.Close() data, err := io.ReadAll(file) if err != nil { writeError(w, http.StatusInternalServerError, "failed to read file") return } // Decode image. src, _, err := image.Decode(bytes.NewReader(data)) if err != nil { writeError(w, http.StatusBadRequest, "invalid image format") return } // Resize to fit within avatarMaxDimension. bounds := src.Bounds() wDim, hDim := bounds.Dx(), bounds.Dy() if wDim > avatarMaxDimension || hDim > avatarMaxDimension { ratio := float64(avatarMaxDimension) / float64(max(wDim, hDim)) newW := int(float64(wDim) * ratio) newH := int(float64(hDim) * ratio) dst := image.NewRGBA(image.Rect(0, 0, newW, newH)) draw.ApproxBiLinear.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil) src = dst } // Encode as JPEG. var buf bytes.Buffer if err := jpeg.Encode(&buf, src, &jpeg.Options{Quality: avatarQuality}); err != nil { writeError(w, http.StatusInternalServerError, "failed to encode avatar") return } avatarRel := fmt.Sprintf("avatars/%d_%d%s", user.ID, time.Now().Unix(), ".jpg") if err := a.Storage.StoreAt(avatarRel, buf.Bytes()); err != nil { writeError(w, http.StatusInternalServerError, "failed to store avatar") return } // Delete old avatar if exists. if user.AvatarPath != nil { a.Storage.Delete(*user.AvatarPath) } // Update DB. if err := a.DB.SetUserAvatar(r.Context(), user.ID, &avatarRel); err != nil { writeError(w, http.StatusInternalServerError, "failed to save avatar") return } avatarURL := "/api/users/" + strconv.FormatInt(user.ID, 10) + "/avatar" writeJSON(w, http.StatusOK, map[string]any{ "avatar_url": avatarURL, }) } func (a *API) HandleServeAvatar(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid user id") return } user, err := a.DB.GetUserByID(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, "user not found") return } if user.AvatarPath == nil { writeError(w, http.StatusNotFound, "no avatar") return } absPath := a.Storage.AbsPath(*user.AvatarPath) w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "public, max-age=86400") http.ServeFile(w, r, absPath) }