Compare commits
3 Commits
8220922cc9
...
576359798f
| Author | SHA1 | Date | |
|---|---|---|---|
| 576359798f | |||
| 8efcc9df4b | |||
| c29a57d54d |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@ -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
|
||||||
@ -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
94
main.go
@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user