+59
-8
internal/cache/cache.go
+59
-8
internal/cache/cache.go
···
1
1
package cache
2
2
3
3
import (
4
+
"crypto/sha256"
5
+
"encoding/hex"
4
6
"os"
5
7
"path/filepath"
6
8
"strings"
···
19
21
return nil, err
20
22
}
21
23
return &Cache{storePath: storePath}, nil
24
+
}
25
+
26
+
// hashBucketPath returns a 3-level deep directory path based on SHA256 of the DID
27
+
func hashBucketPath(did string) string {
28
+
hash := sha256.Sum256([]byte(did))
29
+
hexHash := hex.EncodeToString(hash[:])
30
+
return filepath.Join(hexHash[0:2], hexHash[2:4], hexHash[4:6])
22
31
}
23
32
24
33
// didToFilename converts a DID to a safe filename
···
26
35
return strings.ReplaceAll(did, ":", "_") + ".jpg"
27
36
}
28
37
38
+
// didToThumbFilename converts a DID to a thumbnail filename
39
+
func didToThumbFilename(did string) string {
40
+
return strings.ReplaceAll(did, ":", "_") + "_thumb.jpg"
41
+
}
42
+
29
43
// Get retrieves an avatar from the cache
30
44
func (c *Cache) Get(did string) ([]byte, bool) {
31
45
c.mu.RLock()
32
46
defer c.mu.RUnlock()
33
47
34
-
path := filepath.Join(c.storePath, didToFilename(did))
48
+
path := filepath.Join(c.storePath, hashBucketPath(did), didToFilename(did))
49
+
data, err := os.ReadFile(path)
50
+
if err != nil {
51
+
return nil, false
52
+
}
53
+
return data, true
54
+
}
55
+
56
+
// GetThumbnail retrieves a cached thumbnail from the cache
57
+
func (c *Cache) GetThumbnail(did string) ([]byte, bool) {
58
+
c.mu.RLock()
59
+
defer c.mu.RUnlock()
60
+
61
+
path := filepath.Join(c.storePath, hashBucketPath(did), didToThumbFilename(did))
35
62
data, err := os.ReadFile(path)
36
63
if err != nil {
37
64
return nil, false
···
44
71
c.mu.Lock()
45
72
defer c.mu.Unlock()
46
73
47
-
path := filepath.Join(c.storePath, didToFilename(did))
74
+
bucketDir := filepath.Join(c.storePath, hashBucketPath(did))
75
+
if err := os.MkdirAll(bucketDir, 0755); err != nil {
76
+
return err
77
+
}
78
+
path := filepath.Join(bucketDir, didToFilename(did))
79
+
return os.WriteFile(path, data, 0644)
80
+
}
81
+
82
+
// SetThumbnail stores a thumbnail in the cache
83
+
func (c *Cache) SetThumbnail(did string, data []byte) error {
84
+
c.mu.Lock()
85
+
defer c.mu.Unlock()
86
+
87
+
bucketDir := filepath.Join(c.storePath, hashBucketPath(did))
88
+
if err := os.MkdirAll(bucketDir, 0755); err != nil {
89
+
return err
90
+
}
91
+
path := filepath.Join(bucketDir, didToThumbFilename(did))
48
92
return os.WriteFile(path, data, 0644)
49
93
}
50
94
51
-
// Delete removes an avatar from the cache
95
+
// Delete removes an avatar and its thumbnail from the cache
52
96
func (c *Cache) Delete(did string) error {
53
97
c.mu.Lock()
54
98
defer c.mu.Unlock()
55
99
56
-
path := filepath.Join(c.storePath, didToFilename(did))
57
-
err := os.Remove(path)
58
-
if os.IsNotExist(err) {
59
-
return nil
100
+
bucketDir := filepath.Join(c.storePath, hashBucketPath(did))
101
+
102
+
avatarPath := filepath.Join(bucketDir, didToFilename(did))
103
+
if err := os.Remove(avatarPath); err != nil && !os.IsNotExist(err) {
104
+
return err
60
105
}
61
-
return err
106
+
107
+
thumbPath := filepath.Join(bucketDir, didToThumbFilename(did))
108
+
if err := os.Remove(thumbPath); err != nil && !os.IsNotExist(err) {
109
+
return err
110
+
}
111
+
112
+
return nil
62
113
}
+25
-12
internal/server/server.go
+25
-12
internal/server/server.go
···
81
81
did, err := s.fetcher.ResolveDID(ctx, identifier)
82
82
if err != nil {
83
83
log.Printf("Error resolving DID for %s: %v", identifier, err)
84
-
http.Error(w, "Not found", http.StatusNotFound)
84
+
s.serveAvatar(w, GetDefaultAvatar(), false)
85
85
return
86
86
}
87
87
···
93
93
result, err := s.fetcher.Fetch(ctx, identifier)
94
94
if err != nil {
95
95
log.Printf("Error fetching avatar for %s: %v", identifier, err)
96
-
http.Error(w, "Internal server error", http.StatusInternalServerError)
96
+
s.serveAvatar(w, GetDefaultAvatar(), false)
97
97
return
98
98
}
99
99
···
128
128
did, err := s.fetcher.ResolveDID(ctx, identifier)
129
129
if err != nil {
130
130
log.Printf("Error resolving DID for %s: %v", identifier, err)
131
-
http.Error(w, "Not found", http.StatusNotFound)
131
+
s.serveDefaultThumbnail(w)
132
+
return
133
+
}
134
+
135
+
if data, ok := s.cache.GetThumbnail(did); ok {
136
+
s.serveAvatar(w, data, true)
132
137
return
133
138
}
134
139
···
139
144
result, err := s.fetcher.Fetch(ctx, identifier)
140
145
if err != nil {
141
146
log.Printf("Error fetching avatar for %s: %v", identifier, err)
142
-
http.Error(w, "Internal server error", http.StatusInternalServerError)
147
+
s.serveDefaultThumbnail(w)
143
148
return
144
149
}
145
150
146
151
if !result.HasAvatar {
147
-
thumbnail, err := avatar.ScaleToSize(GetDefaultAvatar(), 128, 128)
148
-
if err != nil {
149
-
log.Printf("Error scaling default avatar: %v", err)
150
-
http.Error(w, "Internal server error", http.StatusInternalServerError)
151
-
return
152
-
}
153
-
s.serveAvatar(w, thumbnail, false)
152
+
s.serveDefaultThumbnail(w)
154
153
return
155
154
}
156
155
···
163
162
thumbnail, err := avatar.ScaleToSize(avatarData, 128, 128)
164
163
if err != nil {
165
164
log.Printf("Error scaling avatar to thumbnail: %v", err)
166
-
http.Error(w, "Internal server error", http.StatusInternalServerError)
165
+
s.serveDefaultThumbnail(w)
167
166
return
168
167
}
169
168
169
+
if err := s.cache.SetThumbnail(did, thumbnail); err != nil {
170
+
log.Printf("Error caching thumbnail for %s: %v", did, err)
171
+
}
172
+
170
173
s.serveAvatar(w, thumbnail, true)
171
174
}
172
175
···
178
181
w.Header().Set("Cache-Control", "no-cache")
179
182
}
180
183
w.Write(data)
184
+
}
185
+
186
+
func (s *Server) serveDefaultThumbnail(w http.ResponseWriter) {
187
+
thumbnail, err := avatar.ScaleToSize(GetDefaultAvatar(), 128, 128)
188
+
if err != nil {
189
+
log.Printf("Error scaling default avatar to thumbnail: %v", err)
190
+
http.Error(w, "Internal server error", http.StatusInternalServerError)
191
+
return
192
+
}
193
+
s.serveAvatar(w, thumbnail, false)
181
194
}
182
195
183
196
func handleHealthcheck(w http.ResponseWriter, r *http.Request) {