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 = 256
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}