Skip to content

Commit 4b3db14

Browse files
committed
Initial Commit
0 parents  commit 4b3db14

16 files changed

Lines changed: 599 additions & 0 deletions
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Build and Publish Docker Image
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
tags:
8+
- 'v*'
9+
10+
jobs:
11+
build:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v2
17+
18+
- name: Set up Docker Buildx
19+
uses: docker/setup-buildx-action@v1
20+
21+
- name: Log in to GitHub Container Registry
22+
uses: docker/login-action@v2
23+
with:
24+
registry: ghcr.io
25+
username: ${{ github.actor }}
26+
password: ${{ secrets.GITHUB_TOKEN }}
27+
28+
- name: Build Docker image
29+
run: |
30+
docker build -t ghcr.io/${{ github.repository_owner }}/skinatar:${{ github.sha }} .
31+
32+
- name: Push Docker image
33+
run: |
34+
docker push ghcr.io/${{ github.repository_owner }}/skinatar:${{ github.sha }}
35+
36+
- name: Tag Docker image with version
37+
if: startsWith(github.ref, 'refs/tags')
38+
run: |
39+
docker tag ghcr.io/${{ github.repository_owner }}/skinatar:${{ github.sha }} ghcr.io/${{ github.repository_owner }}/skinatar:${{ github.ref_name }}
40+
docker push ghcr.io/${{ github.repository_owner }}/skinatar:${{ github.ref_name }}
41+
42+
- name: Tag Docker image as latest
43+
run: |
44+
docker tag ghcr.io/${{ github.repository_owner }}/skinatar:${{ github.sha }} ghcr.io/${{ github.repository_owner }}/skinatar:latest
45+
docker push ghcr.io/${{ github.repository_owner }}/skinatar:latest

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.idea
2+
cached_skins
3+
*.iml
4+
main

Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM golang:1.23.1-alpine AS build
2+
3+
WORKDIR /app
4+
COPY . /app
5+
RUN go mod download
6+
RUN go build -o skinatar
7+
8+
FROM alpine:latest
9+
10+
COPY --from=build /app/skinatar /usr/local/bin/skinatar
11+
RUN ls -l /usr/local/bin/skinatar
12+
RUN chmod +x /usr/local/bin/skinatar
13+
14+
EXPOSE 8080
15+
ENV REDIS_URL="localhost:6379"
16+
VOLUME /app/cached_skins
17+
18+
CMD ["/usr/local/bin/skinatar"]

cache.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"github.com/go-redis/redis/v8"
6+
"os"
7+
"time"
8+
)
9+
10+
/*
11+
Redis Cache Store, to store Username to UUIDs and Texture Paths
12+
*/
13+
var ctx = context.Background()
14+
var redisClient *redis.Client
15+
16+
// Initialize Redis Client
17+
func initRedis() {
18+
redisClient = redis.NewClient(&redis.Options{
19+
Addr: os.Getenv("REDIS_URL"),
20+
DB: 0,
21+
})
22+
}
23+
24+
// Helper function to store skins in redis cache
25+
func cacheSkin(uuid string, cachePath string) {
26+
redisClient.Set(ctx, "skin:"+uuid, cachePath, 1*time.Hour)
27+
}
28+
29+
// Helper function to store usernames in redis cache
30+
func cacheUsername(username string, uuid string) {
31+
redisClient.Set(ctx, "username:"+username, uuid, 1*time.Hour)
32+
}

cleanup.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"time"
8+
)
9+
10+
func cleanupCache() {
11+
now := time.Now()
12+
13+
err := filepath.Walk(skinCacheDir, func(path string, info os.FileInfo, err error) error {
14+
if err != nil {
15+
return err
16+
}
17+
18+
if !info.IsDir() {
19+
age := now.Sub(info.ModTime())
20+
21+
if age > time.Hour {
22+
err := os.Remove(path)
23+
if err != nil {
24+
fmt.Printf("Failed to remove file %s: %v\n", path, err)
25+
} else {
26+
fmt.Printf("Removed expired file: %s\n", path)
27+
}
28+
}
29+
}
30+
return nil
31+
})
32+
33+
if err != nil {
34+
fmt.Printf("Error while cleaning up cache: %v\n", err)
35+
}
36+
}
37+
38+
func startCleanupRoutine() {
39+
ticker := time.NewTicker(time.Hour)
40+
defer ticker.Stop()
41+
42+
for {
43+
select {
44+
case <-ticker.C:
45+
cleanupCache()
46+
}
47+
}
48+
}

docker-compose.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
services:
2+
skinatar_watchtower:
3+
image: containrrr/watchtower
4+
command:
5+
- "--label-enable"
6+
- "--interval"
7+
- "30"
8+
- "--rolling-restart"
9+
volumes:
10+
- /var/run/docker.sock:/var/run/docker.sock
11+
12+
skinatar_cache:
13+
image: redis:alpine
14+
container_name: "skinatar_cache"
15+
restart: unless-stopped
16+
networks:
17+
- skinatar_docker
18+
19+
skinatar:
20+
image: ghcr.io/firstdarkdev/skinatar:latest
21+
restart: always
22+
labels:
23+
- "com.centurylinklabs.watchtower.enable=true"
24+
ports:
25+
- 8080
26+
volumes:
27+
- ./storage:/app/cached_skins
28+
environment:
29+
REDIS_URL: skinatar_cache:5698
30+
networks:
31+
- skinatar_docker
32+
33+
networks:
34+
skinatar_docker:

fallback.png

1.16 KB
Loading

go.mod

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module main
2+
3+
go 1.23.0
4+
5+
require (
6+
github.com/go-redis/redis/v8 v8.11.5
7+
github.com/google/uuid v1.6.0
8+
github.com/gorilla/mux v1.8.1
9+
github.com/mineatar-io/skin-render v1.3.1
10+
)
11+
12+
require (
13+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
14+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
15+
golang.org/x/net v0.38.0 // indirect
16+
golang.org/x/sys v0.31.0 // indirect
17+
)

go.sum

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
2+
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
3+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
4+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
5+
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
6+
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
7+
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
8+
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
9+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
10+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
11+
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
12+
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
13+
github.com/mineatar-io/skin-render v1.3.1 h1:RHSParpSZkdFhJhU/P8aKKugufHqqVEteT4X+zjkTIQ=
14+
github.com/mineatar-io/skin-render v1.3.1/go.mod h1:ESYvjLHUilplx/WhI3fNCfbvAGhiPL0kC273tIdZ8WA=
15+
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
16+
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
17+
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
18+
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
19+
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
20+
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
21+
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
22+
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
23+
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
24+
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
25+
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
26+
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
27+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
28+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
29+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
30+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

http.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"github.com/gorilla/mux"
6+
"image/png"
7+
"net/http"
8+
"strconv"
9+
"strings"
10+
)
11+
12+
// Init the HTTP (or rest api) server
13+
func initHttp() {
14+
r := mux.NewRouter()
15+
r.HandleFunc("/{type}/{id}", handleAvatar).Methods("GET")
16+
17+
http.ListenAndServe(":8080", r)
18+
}
19+
20+
// Handle the incoming avatar request.
21+
func handleAvatar(w http.ResponseWriter, r *http.Request) {
22+
// Default values passed from URL
23+
vars := mux.Vars(r)
24+
identifier := vars["id"]
25+
mode := vars["type"]
26+
scaleStr := r.URL.Query().Get("scale")
27+
28+
// Double check that the requested render is supported
29+
if mode != "isometric" && mode != "body" && mode != "avatar" {
30+
http.Error(w, "Unsupported render mode. Valid render modes are isometric, body & avatar", http.StatusBadRequest)
31+
return
32+
}
33+
34+
// Initialize default scale
35+
scale := 512
36+
37+
// Parse scale from string to int, if possible
38+
if scaleStr != "" {
39+
if parsedScale, err := strconv.Atoi(scaleStr); err == nil {
40+
scale = parsedScale
41+
} else {
42+
fmt.Println("Invalid scale value, using default")
43+
}
44+
}
45+
46+
var uuid string
47+
var err error
48+
49+
// Check if the supplied ID is a valid UUID
50+
if isValidUUID(identifier) {
51+
// We don't need the - in the UUID, so we remove it
52+
uuid = strings.ReplaceAll(identifier, "-", "")
53+
54+
// Check if the supplied ID is a texture hash
55+
} else if isValidSHA256Hash(identifier) {
56+
uuid = identifier
57+
} else {
58+
59+
// Supplied ID was likely a username. So we try to resolve it
60+
uuid, err = getUUID(identifier)
61+
if err != nil || uuid == "" {
62+
uuid = strings.ReplaceAll(generateOfflineUUID(identifier).String(), "-", "")
63+
}
64+
}
65+
66+
// Request the skin from the MOJANG servers
67+
skinPath, err := fetchSkin(uuid)
68+
if err != nil {
69+
skinPath = "fallback.png"
70+
}
71+
72+
// Render the skin for the API
73+
img, err := renderSkin(skinPath, mode, scale, uuid, true)
74+
if err != nil {
75+
http.Error(w, "Failed to render skin", http.StatusInternalServerError)
76+
return
77+
}
78+
79+
// Encode the image ready for browser rendering
80+
w.Header().Set("Content-Type", "image/png")
81+
err = png.Encode(w, img)
82+
if err != nil {
83+
http.Error(w, "Failed to encode image", http.StatusInternalServerError)
84+
return
85+
}
86+
}

0 commit comments

Comments
 (0)