all repos — weimar @ 088c2e33b9850fd8e9a0a65e6876d579b0f5433f

Unnamed repository; edit this file 'description' to name the repository.

internal/api/avatar.go (view raw)

  1package api
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"image"
  7	"image/jpeg"
  8	"io"
  9	"net/http"
 10	"strconv"
 11	"time"
 12
 13	"golang.org/x/image/draw"
 14)
 15
 16const (
 17	avatarMaxDimension = 128
 18	avatarQuality      = 85
 19)
 20
 21func (a *API) HandleUploadAvatar(w http.ResponseWriter, r *http.Request) {
 22	user := UserFromContext(r.Context())
 23	if user == nil {
 24		writeError(w, http.StatusUnauthorized, "unauthorized")
 25		return
 26	}
 27
 28	if err := r.ParseMultipartForm(8 << 20); err != nil {
 29		writeError(w, http.StatusBadRequest, "invalid multipart form")
 30		return
 31	}
 32
 33	file, _, err := r.FormFile("avatar")
 34	if err != nil {
 35		writeError(w, http.StatusBadRequest, "avatar field is required")
 36		return
 37	}
 38	defer file.Close()
 39
 40	data, err := io.ReadAll(file)
 41	if err != nil {
 42		writeError(w, http.StatusInternalServerError, "failed to read file")
 43		return
 44	}
 45
 46	// Decode image.
 47	src, _, err := image.Decode(bytes.NewReader(data))
 48	if err != nil {
 49		writeError(w, http.StatusBadRequest, "invalid image format")
 50		return
 51	}
 52
 53	// Resize to fit within avatarMaxDimension.
 54	bounds := src.Bounds()
 55	wDim, hDim := bounds.Dx(), bounds.Dy()
 56	if wDim > avatarMaxDimension || hDim > avatarMaxDimension {
 57		ratio := float64(avatarMaxDimension) / float64(max(wDim, hDim))
 58		newW := int(float64(wDim) * ratio)
 59		newH := int(float64(hDim) * ratio)
 60		dst := image.NewRGBA(image.Rect(0, 0, newW, newH))
 61		draw.ApproxBiLinear.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil)
 62		src = dst
 63	}
 64
 65	// Encode as JPEG.
 66	var buf bytes.Buffer
 67	if err := jpeg.Encode(&buf, src, &jpeg.Options{Quality: avatarQuality}); err != nil {
 68		writeError(w, http.StatusInternalServerError, "failed to encode avatar")
 69		return
 70	}
 71
 72	avatarRel := fmt.Sprintf("avatars/%d_%d%s", user.ID, time.Now().Unix(), ".jpg")
 73	if err := a.Storage.StoreAt(avatarRel, buf.Bytes()); err != nil {
 74		writeError(w, http.StatusInternalServerError, "failed to store avatar")
 75		return
 76	}
 77
 78	// Delete old avatar if exists.
 79	if user.AvatarPath != nil {
 80		a.Storage.Delete(*user.AvatarPath)
 81	}
 82
 83	// Update DB.
 84	if err := a.DB.SetUserAvatar(r.Context(), user.ID, &avatarRel); err != nil {
 85		writeError(w, http.StatusInternalServerError, "failed to save avatar")
 86		return
 87	}
 88
 89	avatarURL := "/api/users/" + strconv.FormatInt(user.ID, 10) + "/avatar"
 90	writeJSON(w, http.StatusOK, map[string]any{
 91		"avatar_url": avatarURL,
 92	})
 93}
 94
 95func (a *API) HandleServeAvatar(w http.ResponseWriter, r *http.Request) {
 96	idStr := r.PathValue("id")
 97	id, err := strconv.ParseInt(idStr, 10, 64)
 98	if err != nil {
 99		writeError(w, http.StatusBadRequest, "invalid user id")
100		return
101	}
102
103	user, err := a.DB.GetUserByID(r.Context(), id)
104	if err != nil {
105		writeError(w, http.StatusNotFound, "user not found")
106		return
107	}
108
109	if user.AvatarPath == nil {
110		writeError(w, http.StatusNotFound, "no avatar")
111		return
112	}
113
114	absPath := a.Storage.AbsPath(*user.AvatarPath)
115	w.Header().Set("Content-Type", "image/jpeg")
116	w.Header().Set("Cache-Control", "public, max-age=86400")
117	http.ServeFile(w, r, absPath)
118}