More code cleanup
This commit is contained in:
@@ -1,12 +1,7 @@
|
||||
environment: development
|
||||
host: 127.0.0.1
|
||||
port: 3000
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
user:
|
||||
password:
|
||||
database: 0
|
||||
redis: redis://127.0.0.1:6379/0
|
||||
routes:
|
||||
face:
|
||||
default_overlay: true
|
||||
|
||||
29
src/cache.go
29
src/cache.go
@@ -1,12 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"log"
|
||||
)
|
||||
|
||||
type ResultCacheKey struct {
|
||||
@@ -31,9 +28,7 @@ func GetResultCacheKey(uuid, renderType string, opts *QueryParams) (string, erro
|
||||
return "", err
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(rawKeyData)
|
||||
|
||||
return fmt.Sprintf("result:%s", hex.EncodeToString(hash[:])), nil
|
||||
return fmt.Sprintf("result:%s", SHA256(rawKeyData)), nil
|
||||
}
|
||||
|
||||
// GetCachedRenderResult returns the render result from Redis cache, or nil if it does not exist or cache is disabled.
|
||||
@@ -81,12 +76,26 @@ func GetCachedSkin(uuid string) (*image.NRGBA, bool, error) {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if config.Environment == "development" {
|
||||
log.Printf("Retrieved player skin from cache (uuid=%s, slim=%v)\n", uuid, slim)
|
||||
}
|
||||
|
||||
return cache, slim, nil
|
||||
}
|
||||
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func SetCachedSkin(uuid string, value []byte, isSlim bool) error {
|
||||
if err := r.Set(fmt.Sprintf("skin:%s", uuid), value, *config.Cache.SkinCacheDuration); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isSlim {
|
||||
if err := r.Set(fmt.Sprintf("slim:%s", uuid), "true", *config.Cache.SkinCacheDuration); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := r.Delete(fmt.Sprintf("slim:%s", uuid)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,14 +13,8 @@ var (
|
||||
Environment: "development",
|
||||
Host: "127.0.0.1",
|
||||
Port: 3001,
|
||||
Redis: RedisConfig{
|
||||
Host: "127.0.0.1",
|
||||
Port: 6379,
|
||||
User: "",
|
||||
Password: "",
|
||||
Database: 0,
|
||||
},
|
||||
Routes: RoutesConfig{
|
||||
Redis: "redis://127.0.0.1:6379/0",
|
||||
Routes: Routes{
|
||||
Face: RouteConfig{
|
||||
DefaultOverlay: true,
|
||||
DefaultDownload: false,
|
||||
@@ -87,13 +81,13 @@ type Config struct {
|
||||
Environment string `yaml:"environment"`
|
||||
Host string `yaml:"host"`
|
||||
Port uint16 `yaml:"port"`
|
||||
Redis RedisConfig `yaml:"redis"`
|
||||
Routes RoutesConfig `yaml:"routes"`
|
||||
Redis string `yaml:"redis"`
|
||||
Routes Routes `yaml:"routes"`
|
||||
Cache CacheConfig `yaml:"cache"`
|
||||
}
|
||||
|
||||
// RoutesConfig is the configuration data of all API routes.
|
||||
type RoutesConfig struct {
|
||||
// Routes is the configuration data of all API routes.
|
||||
type Routes struct {
|
||||
Face RouteConfig `yaml:"face"`
|
||||
Head RouteConfig `yaml:"head"`
|
||||
FullBody RouteConfig `yaml:"full_body"`
|
||||
@@ -113,15 +107,6 @@ type RouteConfig struct {
|
||||
MaxScale int `yaml:"max_scale"`
|
||||
}
|
||||
|
||||
// RedisConfig is the configuration data used to connect to Redis.
|
||||
type RedisConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
Port uint16 `yaml:"port"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
Database int `yaml:"database"`
|
||||
}
|
||||
|
||||
// CacheConfig is the configuration data used to set TTL values for Redis keys.
|
||||
type CacheConfig struct {
|
||||
SkinCacheDuration *time.Duration `yaml:"skin_cache_duration"`
|
||||
|
||||
@@ -20,8 +20,8 @@ type MinecraftProfile struct {
|
||||
} `json:"properties"`
|
||||
}
|
||||
|
||||
// MinecraftDecodedTextures is the decoded object of the base64-encoded values property in a MinecraftProfile properties value.
|
||||
type MinecraftDecodedTextures struct {
|
||||
// DecodedTextures is the decoded object of the base64-encoded values property in a MinecraftProfile properties value.
|
||||
type DecodedTextures struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
UUID string `json:"uuid"`
|
||||
Username string `json:"username"`
|
||||
@@ -47,7 +47,7 @@ func GetMinecraftProfile(uuid string) (*MinecraftProfile, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "mineatar.io Skin Render API")
|
||||
req.Header.Set("User-Agent", "mineatar.io")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
|
||||
@@ -71,7 +71,7 @@ func GetMinecraftProfile(uuid string) (*MinecraftProfile, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := MinecraftProfile{}
|
||||
var response MinecraftProfile
|
||||
|
||||
if err = json.Unmarshal(body, &response); err != nil {
|
||||
return nil, err
|
||||
@@ -80,15 +80,15 @@ func GetMinecraftProfile(uuid string) (*MinecraftProfile, error) {
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// GetDecodedTexturesValue decodes the values from a MinecraftProfileTextures texture value.
|
||||
func GetDecodedTexturesValue(value string) (*MinecraftDecodedTextures, error) {
|
||||
// DecodeTexturesValue decodes the value from a MinecraftProfile texture property.
|
||||
func DecodeTexturesValue(value string) (*DecodedTextures, error) {
|
||||
rawResult, err := base64.StdEncoding.DecodeString(value)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := MinecraftDecodedTextures{}
|
||||
var result DecodedTextures
|
||||
|
||||
if err = json.Unmarshal(rawResult, &result); err != nil {
|
||||
return nil, err
|
||||
|
||||
16
src/redis.go
16
src/redis.go
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"time"
|
||||
@@ -24,17 +23,18 @@ type Redis struct {
|
||||
}
|
||||
|
||||
// Connect connects to the Redis server using the configuration values provided.
|
||||
func (r *Redis) Connect(conf RedisConfig) error {
|
||||
func (r *Redis) Connect(url string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
|
||||
defer cancel()
|
||||
|
||||
r.Client = redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", conf.Host, conf.Port),
|
||||
Username: conf.User,
|
||||
Password: conf.Password,
|
||||
DB: conf.Database,
|
||||
})
|
||||
opts, err := redis.ParseURL(url)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Client = redis.NewClient(opts)
|
||||
|
||||
if err := r.Client.Ping(ctx).Err(); err != nil {
|
||||
return err
|
||||
|
||||
123
src/renderer.go
Normal file
123
src/renderer.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/mineatar-io/skin-render"
|
||||
)
|
||||
|
||||
var (
|
||||
RenderTypeFullBody = "fullbody"
|
||||
RenderTypeFrontBody = "frontbody"
|
||||
RenderTypeBackBody = "backbody"
|
||||
RenderTypeLeftBody = "leftbody"
|
||||
RenderTypeRightBody = "rightbody"
|
||||
RenderTypeFace = "face"
|
||||
RenderTypeHead = "head"
|
||||
)
|
||||
|
||||
// Render will render the image using the specified details and return the result.
|
||||
func Render(renderType, uuid string, rawSkin *image.NRGBA, isSlim bool, opts *QueryParams) ([]byte, bool, error) {
|
||||
if config.Cache.EnableLocks {
|
||||
mutex := r.NewMutex(fmt.Sprintf("render-lock:%s-%d-%t-%s", renderType, opts.Scale, opts.Overlay, uuid))
|
||||
mutex.Lock()
|
||||
|
||||
defer mutex.Unlock()
|
||||
}
|
||||
|
||||
// Fetch the existing render from cache if it exists
|
||||
{
|
||||
cache, err := GetCachedRenderResult(renderType, uuid, opts)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if cache != nil {
|
||||
return cache, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
result *image.NRGBA
|
||||
renderOpts skin.Options = skin.Options{
|
||||
Overlay: opts.Overlay,
|
||||
Slim: isSlim,
|
||||
Scale: opts.Scale,
|
||||
}
|
||||
)
|
||||
|
||||
// Render the image based on the type provided
|
||||
{
|
||||
switch renderType {
|
||||
case RenderTypeFullBody:
|
||||
{
|
||||
result = skin.RenderBody(rawSkin, renderOpts)
|
||||
|
||||
break
|
||||
}
|
||||
case RenderTypeFrontBody:
|
||||
{
|
||||
result = skin.RenderFrontBody(rawSkin, renderOpts)
|
||||
|
||||
break
|
||||
}
|
||||
case RenderTypeBackBody:
|
||||
{
|
||||
result = skin.RenderBackBody(rawSkin, renderOpts)
|
||||
|
||||
break
|
||||
}
|
||||
case RenderTypeLeftBody:
|
||||
{
|
||||
result = skin.RenderLeftBody(rawSkin, renderOpts)
|
||||
|
||||
break
|
||||
}
|
||||
case RenderTypeRightBody:
|
||||
{
|
||||
result = skin.RenderRightBody(rawSkin, renderOpts)
|
||||
|
||||
break
|
||||
}
|
||||
case RenderTypeHead:
|
||||
{
|
||||
result = skin.RenderHead(rawSkin, renderOpts)
|
||||
|
||||
break
|
||||
}
|
||||
case RenderTypeFace:
|
||||
{
|
||||
result = skin.RenderFace(rawSkin, renderOpts)
|
||||
|
||||
break
|
||||
}
|
||||
default:
|
||||
panic(fmt.Errorf("unknown render type: %s", renderType))
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
data []byte
|
||||
err error
|
||||
)
|
||||
|
||||
// Encode the image into a PNG in byte-array format
|
||||
{
|
||||
data, err = EncodePNG(result)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
// Put the result into the cache for later use
|
||||
{
|
||||
if err = SetCachedRenderResult(renderType, uuid, opts, data); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
return data, false, nil
|
||||
}
|
||||
133
src/util.go
133
src/util.go
@@ -2,7 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
@@ -20,14 +22,6 @@ import (
|
||||
var (
|
||||
//go:embed favicon.ico
|
||||
favicon []byte
|
||||
|
||||
RenderTypeFullBody = "fullbody"
|
||||
RenderTypeFrontBody = "frontbody"
|
||||
RenderTypeBackBody = "backbody"
|
||||
RenderTypeLeftBody = "leftbody"
|
||||
RenderTypeRightBody = "rightbody"
|
||||
RenderTypeFace = "face"
|
||||
RenderTypeHead = "head"
|
||||
)
|
||||
|
||||
// QueryParams is used by most all API routes as options for how the image should be rendered, or how errors should be handled.
|
||||
@@ -56,102 +50,6 @@ func Clamp[T int | uint | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64
|
||||
return value
|
||||
}
|
||||
|
||||
// Render will render the image using the specified details and return the result.
|
||||
func Render(renderType, uuid string, rawSkin *image.NRGBA, isSlim bool, opts *QueryParams) ([]byte, bool, error) {
|
||||
if config.Cache.EnableLocks {
|
||||
mutex := r.NewMutex(fmt.Sprintf("render-lock:%s-%d-%t-%s", renderType, opts.Scale, opts.Overlay, uuid))
|
||||
mutex.Lock()
|
||||
|
||||
defer mutex.Unlock()
|
||||
}
|
||||
|
||||
cache, err := GetCachedRenderResult(renderType, uuid, opts)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if cache != nil {
|
||||
if config.Environment == "development" {
|
||||
log.Printf("Retrieved render from cache (type=%s, uuid=%s, slim=%v, scale=%d)\n", renderType, uuid, isSlim, opts.Scale)
|
||||
}
|
||||
|
||||
return cache, true, nil
|
||||
}
|
||||
|
||||
var (
|
||||
result *image.NRGBA
|
||||
renderOpts skin.Options = skin.Options{
|
||||
Overlay: opts.Overlay,
|
||||
Slim: isSlim,
|
||||
Scale: opts.Scale,
|
||||
}
|
||||
)
|
||||
|
||||
switch renderType {
|
||||
case RenderTypeFullBody:
|
||||
{
|
||||
result = skin.RenderBody(rawSkin, renderOpts)
|
||||
|
||||
break
|
||||
}
|
||||
case RenderTypeFrontBody:
|
||||
{
|
||||
result = skin.RenderFrontBody(rawSkin, renderOpts)
|
||||
|
||||
break
|
||||
}
|
||||
case RenderTypeBackBody:
|
||||
{
|
||||
result = skin.RenderBackBody(rawSkin, renderOpts)
|
||||
|
||||
break
|
||||
}
|
||||
case RenderTypeLeftBody:
|
||||
{
|
||||
result = skin.RenderLeftBody(rawSkin, renderOpts)
|
||||
|
||||
break
|
||||
}
|
||||
case RenderTypeRightBody:
|
||||
{
|
||||
result = skin.RenderRightBody(rawSkin, renderOpts)
|
||||
|
||||
break
|
||||
}
|
||||
case RenderTypeHead:
|
||||
{
|
||||
result = skin.RenderHead(rawSkin, renderOpts)
|
||||
|
||||
break
|
||||
}
|
||||
case RenderTypeFace:
|
||||
{
|
||||
result = skin.RenderFace(rawSkin, renderOpts)
|
||||
|
||||
break
|
||||
}
|
||||
default:
|
||||
panic(fmt.Errorf("unknown render type: %s", renderType))
|
||||
}
|
||||
|
||||
data, err := EncodePNG(result)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if err = SetCachedRenderResult(renderType, uuid, opts, data); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if config.Environment == "development" {
|
||||
log.Printf("Rendered image (type=%s, uuid=%s, slim=%v, scale=%d)\n", renderType, uuid, isSlim, opts.Scale)
|
||||
}
|
||||
|
||||
return data, false, nil
|
||||
}
|
||||
|
||||
// ExtractUUID returns the UUID from the route param, allowing values such as "<uuid>.png" to be returned as "<uuid>".
|
||||
func ExtractUUID(ctx *fiber.Ctx) string {
|
||||
return strings.Split(ctx.Params("uuid"), ".")[0]
|
||||
@@ -224,7 +122,7 @@ func GetPlayerSkin(uuid string) (*image.NRGBA, bool, error) {
|
||||
isSlim bool = skin.IsSlimFromUUID(uuid)
|
||||
profile *MinecraftProfile = nil
|
||||
rawTextures string = ""
|
||||
texturesProperty *MinecraftDecodedTextures = nil
|
||||
texturesProperty *DecodedTextures = nil
|
||||
)
|
||||
|
||||
// Get the textures metadata from Mojang about the Minecraft player
|
||||
@@ -259,7 +157,7 @@ func GetPlayerSkin(uuid string) (*image.NRGBA, bool, error) {
|
||||
|
||||
// Decode the raw textures value returned from the player's properties
|
||||
{
|
||||
if texturesProperty, err = GetDecodedTexturesValue(rawTextures); err != nil {
|
||||
if texturesProperty, err = DecodeTexturesValue(rawTextures); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
@@ -283,23 +181,9 @@ func GetPlayerSkin(uuid string) (*image.NRGBA, bool, error) {
|
||||
|
||||
// Put the skin into cache so it can be used for future requests
|
||||
if config.Cache.SkinCacheDuration != nil {
|
||||
if err = r.Set(fmt.Sprintf("skin:%s", uuid), rawSkin, *config.Cache.SkinCacheDuration); err != nil {
|
||||
if err = SetCachedSkin(uuid, rawSkin, isSlim); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if isSlim {
|
||||
if err = r.Set(fmt.Sprintf("slim:%s", uuid), "true", *config.Cache.SkinCacheDuration); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
} else {
|
||||
if err = r.Delete(fmt.Sprintf("slim:%s", uuid)); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config.Environment == "development" {
|
||||
log.Printf("Fetched player skin from Mojang (uuid=%s, slim=%v)\n", uuid, isSlim)
|
||||
}
|
||||
|
||||
return skinImage, isSlim, nil
|
||||
@@ -357,3 +241,10 @@ func GetInstanceID() (uint16, error) {
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// SHA256 computes the SHA-256 hash of the input byte-array.
|
||||
func SHA256(value []byte) string {
|
||||
hash := sha256.Sum256(value)
|
||||
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user