Compare commits

..

3 Commits

Author SHA1 Message Date
576359798f feat: add cache support
Some checks failed
Publish Docker Image / build_push (push) Failing after 2m19s
2024-08-01 22:29:02 +03:00
8efcc9df4b fix: remove provenance images 2024-08-01 22:28:42 +03:00
c29a57d54d feat: multi platform images 2024-08-01 22:28:25 +03:00
3 changed files with 90 additions and 10 deletions

View File

@ -22,10 +22,12 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Build and push api - name: Build and push api
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
env: env:
ACTIONS_RUNTIME_TOKEN: '' ACTIONS_RUNTIME_TOKEN: ''
with: with:
platforms: linux/amd64,linux/arm64
provenance: false
context: . context: .
push: true push: true
tags: git.lovepin.app/nisan/avatars:latest tags: git.lovepin.app/nisan/avatars:latest

View File

@ -21,7 +21,7 @@ RUN go mod download
RUN go mod verify RUN go mod verify
# Build the binary. # Build the binary.
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/avatars RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o /go/bin/avatars
############################ ############################
# STEP 2 build a small image # STEP 2 build a small image

94
main.go
View File

@ -11,6 +11,7 @@ import (
"io/fs" "io/fs"
"log" "log"
"net/http" "net/http"
"sync"
"strconv" "strconv"
"strings" "strings"
@ -19,6 +20,54 @@ import (
"golang.org/x/image/draw" "golang.org/x/image/draw"
) )
type cacheItem struct {
value []byte
lastAccess int64
}
type AvatarCache struct {
cache map[string]*cacheItem
mu *sync.RWMutex
}
func (ac *AvatarCache) Clear() {
ac.mu.Lock()
for k, v := range ac.cache {
if time.Now().Unix()-v.lastAccess > int64(100) {
delete(ac.cache, k)
}
}
ac.mu.Unlock()
}
// insert to cache
func (ac *AvatarCache) Insert(id string, size int, value []byte) {
ac.mu.Lock()
defer ac.mu.Unlock()
ac.cache[id+strconv.Itoa(size)] = &cacheItem{value: value, lastAccess: time.Now().Unix()}
// remove oldest item if cache is full
if len(ac.cache) > 1000 {
for k := range ac.cache {
delete(ac.cache, k)
break
}
}
}
// get from cache
func (ac *AvatarCache) Get(id string, size int) []byte {
ac.mu.Lock()
defer ac.mu.Unlock()
item, ok := ac.cache[id+strconv.Itoa(size)]
if !ok {
return nil
}
item.lastAccess = time.Now().Unix()
return item.value
}
// create new cache
var avatarCache = AvatarCache{cache: make(map[string]*cacheItem), mu: &sync.RWMutex{}}
//go:embed assets/* //go:embed assets/*
var files embed.FS var files embed.FS
@ -52,15 +101,31 @@ func serveAvatar(w http.ResponseWriter, r *http.Request) {
// get the id and size from the request // get the id and size from the request
id := r.PathValue("id") id := r.PathValue("id")
size, err := strconv.Atoi(r.PathValue("size")) sizeStr := r.PathValue("size")
if sizeStr == "" {
sizeStr = r.URL.Query().Get("s")
}
size, err := strconv.Atoi(sizeStr)
if err != nil { if err != nil {
size = 400 size = 400
} }
// min size is 32, max size is 1000 // min size is 32, max size is 2048
if size < 32 { if size < 32 {
size = 32 size = 32
} else if size > 1000 { } else if size > 2048 {
size = 1000 size = 2048
}
if avatarCache.Get(id, size) != nil {
// get from cache
w.Header().Set("content-type", "image/jpeg")
w.WriteHeader(http.StatusFound)
byteSize, err := w.Write(avatarCache.Get(id, size))
if err != nil {
log.Println(err.Error())
}
log.Printf("[id:%s][size:%d] [%s] [%s] [%d bytes] [%v] [cache hit]\n",
id, size, r.RemoteAddr, r.Proto, byteSize, time.Since(startTime))
return
} }
img := image.NewRGBA(emptyRect) img := image.NewRGBA(emptyRect)
@ -74,26 +139,38 @@ func serveAvatar(w http.ResponseWriter, r *http.Request) {
if size != 400 { if size != 400 {
// resize the image // resize the image
dst := image.NewRGBA(image.Rect(0, 0, size, size)) dst := image.NewRGBA(image.Rect(0, 0, size, size))
draw.ApproxBiLinear.Scale(dst, dst.Bounds(), img, emptyRect, draw.Over, nil) draw.CatmullRom.Scale(dst, dst.Bounds(), img, emptyRect, draw.Over, nil)
img = dst img = dst
} }
// write the image to the response // write the image to the response
w.Header().Set("content-type", "image/jpeg") w.Header().Set("content-type", "image/jpeg")
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
err = jpeg.Encode(w, img, nil) buf := new(bytes.Buffer)
err = jpeg.Encode(buf, img, nil)
if err != nil { if err != nil {
log.Println(err.Error()) log.Println(err.Error())
} }
log.Printf("[id:%s][size:%d] served to [%s] [%s] [%v]\n", id, size, r.RemoteAddr, r.Proto, time.Since(startTime)) byteSize, err := w.Write(buf.Bytes())
if err != nil {
log.Println(err.Error())
}
avatarCache.Insert(id, size, buf.Bytes())
log.Printf("[id:%s][size:%d] [%s] [%s] [%d bytes] [%v]\n",
id, size, r.RemoteAddr, r.Proto, byteSize, time.Since(startTime))
} }
func main() { func main() {
log.Println("assets loaded") go func() {
for range time.Tick(time.Minute) {
avatarCache.Clear()
}
}()
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("GET /{id}", serveAvatar) mux.HandleFunc("GET /{id}", serveAvatar)
mux.HandleFunc("GET /{size}/{id}", serveAvatar) mux.HandleFunc("GET /{size}/{id}", serveAvatar)
http.ListenAndServe("0.0.0.0:3382", mux) http.ListenAndServe("0.0.0.0:3382", mux)
log.Println("Server started at 0.0.0.0:3382")
} }
func init() { func init() {
@ -140,4 +217,5 @@ func init() {
} }
} }
} }
log.Printf("%d different combinations loaded\n", len(BaseRects)*len(EYES)*len(NOSE)*len(MOUTH))
} }