Add format query parameter
This commit is contained in:
@@ -7,46 +7,54 @@ routes:
|
||||
default_overlay: true
|
||||
default_download: false
|
||||
default_scale: 4
|
||||
default_format: png
|
||||
min_scale: 1
|
||||
max_scale: 64
|
||||
head:
|
||||
default_overlay: true
|
||||
default_download: false
|
||||
default_scale: 4
|
||||
default_format: png
|
||||
min_scale: 1
|
||||
max_scale: 64
|
||||
full_body:
|
||||
default_overlay: true
|
||||
default_download: false
|
||||
default_scale: 4
|
||||
default_format: png
|
||||
min_scale: 1
|
||||
max_scale: 64
|
||||
front_body:
|
||||
default_overlay: true
|
||||
default_download: false
|
||||
default_scale: 4
|
||||
default_format: png
|
||||
min_scale: 1
|
||||
max_scale: 64
|
||||
back_body:
|
||||
default_overlay: true
|
||||
default_download: false
|
||||
default_scale: 4
|
||||
default_format: png
|
||||
min_scale: 1
|
||||
max_scale: 64
|
||||
left_body:
|
||||
default_overlay: true
|
||||
default_download: false
|
||||
default_scale: 4
|
||||
default_format: png
|
||||
min_scale: 1
|
||||
max_scale: 64
|
||||
right_body:
|
||||
default_overlay: true
|
||||
default_download: false
|
||||
default_scale: 4
|
||||
default_format: png
|
||||
min_scale: 1
|
||||
max_scale: 64
|
||||
raw_skin:
|
||||
default_download: false
|
||||
default_format: png
|
||||
cache:
|
||||
skin_cache_duration: 12h # 12 hours
|
||||
render_cache_duration: 12h # 12 hours
|
||||
|
||||
14
go.mod
14
go.mod
@@ -1,12 +1,14 @@
|
||||
module github.com/mineatar-io/api-server
|
||||
|
||||
go 1.18
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.0
|
||||
|
||||
require (
|
||||
github.com/go-redsync/redsync/v4 v4.8.1
|
||||
github.com/gofiber/fiber/v2 v2.48.0
|
||||
github.com/gofiber/fiber/v2 v2.49.0
|
||||
github.com/mineatar-io/skin-render v1.0.9
|
||||
github.com/redis/go-redis/v9 v9.0.5
|
||||
github.com/redis/go-redis/v9 v9.1.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -15,18 +17,18 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.48.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
)
|
||||
|
||||
24
go.sum
24
go.sum
@@ -1,9 +1,11 @@
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/bsm/ginkgo/v2 v2.5.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
|
||||
github.com/bsm/ginkgo/v2 v2.9.5 h1:rtVBYPs3+TC5iLUVOis1B9tjLTup7Cj5IfzosKtvTJ0=
|
||||
github.com/bsm/ginkgo/v2 v2.9.5/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.20.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATsbwk=
|
||||
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
||||
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -24,8 +26,8 @@ github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq
|
||||
github.com/go-redsync/redsync/v4 v4.8.1 h1:rq2RvdTI0obznMdxKUWGdmmulo7lS9yCzb8fgDKOlbM=
|
||||
github.com/go-redsync/redsync/v4 v4.8.1/go.mod h1:LmUAsQuQxhzZAoGY7JS6+dNhNmZyonMZiiEDY9plotM=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gofiber/fiber/v2 v2.48.0 h1:cRVMCb9aUJDsyHxGFLwz/sGzDggdailZZyptU9F9cU0=
|
||||
github.com/gofiber/fiber/v2 v2.48.0/go.mod h1:xqJgfqrc23FJuqGOW6DVgi3HyZEm2Mn9pRqUb2kHSX8=
|
||||
github.com/gofiber/fiber/v2 v2.49.0 h1:xBVG2c66GDcWfww56xHvMn52Q0XX7UrSvjj6MD8/5EE=
|
||||
github.com/gofiber/fiber/v2 v2.49.0/go.mod h1:oxpt7wQaEYgdDmq7nMxCGhilYicBLFnZ+jQSJcQDlSE=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
@@ -43,8 +45,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -66,8 +68,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mineatar-io/skin-render v1.0.9 h1:EIdHfj01Rs4P8JUk6kuvCxZG6i46c8LNGJV7EttwqQ8=
|
||||
github.com/mineatar-io/skin-render v1.0.9/go.mod h1:KkgHwrhTIqD73dkmeQwh5k2aHuWS8cDahVmd64gM128=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
@@ -83,8 +85,8 @@ github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps=
|
||||
github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o=
|
||||
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
|
||||
github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY=
|
||||
github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
@@ -134,8 +136,8 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
|
||||
41
src/cache.go
41
src/cache.go
@@ -1,9 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ResultCacheKey struct {
|
||||
@@ -14,21 +15,15 @@ type ResultCacheKey struct {
|
||||
}
|
||||
|
||||
// GetCacheKey returns the key used in the cache based on the rendering options, calculated as an SHA-256 hash.
|
||||
func GetResultCacheKey(uuid, renderType string, opts *QueryParams) (string, error) {
|
||||
rawKey := ResultCacheKey{
|
||||
UUID: uuid,
|
||||
Type: renderType,
|
||||
Scale: opts.Scale,
|
||||
Overlay: opts.Overlay,
|
||||
}
|
||||
func GetResultCacheKey(uuid, renderType string, opts *QueryParams) string {
|
||||
values := &url.Values{}
|
||||
values.Set("uuid", uuid)
|
||||
values.Set("type", renderType)
|
||||
values.Set("scale", strconv.FormatInt(int64(opts.Scale), 10))
|
||||
values.Set("overlay", strconv.FormatBool(opts.Overlay))
|
||||
values.Set("format", opts.Format)
|
||||
|
||||
rawKeyData, err := json.Marshal(rawKey)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("result:%s", SHA256(rawKeyData)), nil
|
||||
return SHA256(values.Encode())
|
||||
}
|
||||
|
||||
// GetCachedRenderResult returns the render result from Redis cache, or nil if it does not exist or cache is disabled.
|
||||
@@ -37,13 +32,7 @@ func GetCachedRenderResult(renderType, uuid string, opts *QueryParams) ([]byte,
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
key, err := GetResultCacheKey(uuid, renderType, opts)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.GetBytes(key)
|
||||
return r.GetBytes(fmt.Sprintf("result:%s", GetResultCacheKey(uuid, renderType, opts)))
|
||||
}
|
||||
|
||||
// SetCachedRenderResult puts the render result into cache, or does nothing is cache is disabled.
|
||||
@@ -52,13 +41,7 @@ func SetCachedRenderResult(renderType, uuid string, opts *QueryParams, data []by
|
||||
return nil
|
||||
}
|
||||
|
||||
key, err := GetResultCacheKey(uuid, renderType, opts)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.Set(key, data, *config.Cache.RenderCacheDuration)
|
||||
return r.Set(fmt.Sprintf("result:%s", GetResultCacheKey(uuid, renderType, opts)), data, *config.Cache.RenderCacheDuration)
|
||||
}
|
||||
|
||||
// GetCachedSkin returns the raw skin of a player by UUID from the cache, also returning if the player has a slim player model.
|
||||
|
||||
@@ -21,6 +21,7 @@ var (
|
||||
DefaultScale: 4,
|
||||
MinScale: 1,
|
||||
MaxScale: 64,
|
||||
DefaultFormat: "png",
|
||||
},
|
||||
Head: RouteConfig{
|
||||
DefaultOverlay: true,
|
||||
@@ -28,6 +29,7 @@ var (
|
||||
DefaultScale: 4,
|
||||
MinScale: 1,
|
||||
MaxScale: 64,
|
||||
DefaultFormat: "png",
|
||||
},
|
||||
FullBody: RouteConfig{
|
||||
DefaultOverlay: true,
|
||||
@@ -35,6 +37,7 @@ var (
|
||||
DefaultScale: 4,
|
||||
MinScale: 1,
|
||||
MaxScale: 64,
|
||||
DefaultFormat: "png",
|
||||
},
|
||||
FrontBody: RouteConfig{
|
||||
DefaultOverlay: true,
|
||||
@@ -42,6 +45,7 @@ var (
|
||||
DefaultScale: 4,
|
||||
MinScale: 1,
|
||||
MaxScale: 64,
|
||||
DefaultFormat: "png",
|
||||
},
|
||||
BackBody: RouteConfig{
|
||||
DefaultOverlay: true,
|
||||
@@ -49,6 +53,7 @@ var (
|
||||
DefaultScale: 4,
|
||||
MinScale: 1,
|
||||
MaxScale: 64,
|
||||
DefaultFormat: "png",
|
||||
},
|
||||
LeftBody: RouteConfig{
|
||||
DefaultOverlay: true,
|
||||
@@ -56,6 +61,7 @@ var (
|
||||
DefaultScale: 4,
|
||||
MinScale: 1,
|
||||
MaxScale: 64,
|
||||
DefaultFormat: "png",
|
||||
},
|
||||
RightBody: RouteConfig{
|
||||
DefaultOverlay: true,
|
||||
@@ -63,9 +69,11 @@ var (
|
||||
DefaultScale: 4,
|
||||
MinScale: 1,
|
||||
MaxScale: 64,
|
||||
DefaultFormat: "png",
|
||||
},
|
||||
RawSkin: RouteConfig{
|
||||
DefaultDownload: false,
|
||||
DefaultFormat: "png",
|
||||
},
|
||||
},
|
||||
Cache: CacheConfig{
|
||||
@@ -103,6 +111,7 @@ type RouteConfig struct {
|
||||
DefaultScale int `yaml:"default_scale"`
|
||||
DefaultOverlay bool `yaml:"default_overlay"`
|
||||
DefaultDownload bool `yaml:"default_download"`
|
||||
DefaultFormat string `yaml:"default_format"`
|
||||
MinScale int `yaml:"min_scale"`
|
||||
MaxScale int `yaml:"max_scale"`
|
||||
}
|
||||
|
||||
27
src/main.go
27
src/main.go
@@ -1,21 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
)
|
||||
|
||||
var (
|
||||
app *fiber.App = fiber.New(fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
ErrorHandler: func(ctx *fiber.Ctx, err error) error {
|
||||
log.Println(ctx.Request().URI(), err)
|
||||
var fiberError *fiber.Error
|
||||
|
||||
if errors.As(err, &fiberError) {
|
||||
return ctx.SendStatus(fiberError.Code)
|
||||
}
|
||||
|
||||
log.Printf("Error: %v - URI: %s\n", err, ctx.Request().URI())
|
||||
|
||||
return ctx.SendStatus(http.StatusInternalServerError)
|
||||
},
|
||||
@@ -38,21 +42,6 @@ func init() {
|
||||
|
||||
log.Println("Successfully connected to Redis")
|
||||
|
||||
app.Use(recover.New())
|
||||
|
||||
if config.Environment == "development" {
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: "*",
|
||||
AllowMethods: "HEAD,OPTIONS,GET",
|
||||
ExposeHeaders: "X-Cache-Hit,X-Cache-Time-Remaining",
|
||||
}))
|
||||
|
||||
app.Use(logger.New(logger.Config{
|
||||
Format: "${time} ${ip}:${port} -> ${status}: ${method} ${path} (${latency})\n",
|
||||
TimeFormat: "2006/01/02 15:04:05",
|
||||
}))
|
||||
}
|
||||
|
||||
if instanceID, err = GetInstanceID(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ func Render(renderType, uuid string, rawSkin *image.NRGBA, isSlim bool, opts *Qu
|
||||
|
||||
// Encode the image into a PNG in byte-array format
|
||||
{
|
||||
data, err = EncodePNG(result)
|
||||
data, err = EncodeImage(result, opts)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
|
||||
137
src/routes.go
137
src/routes.go
@@ -5,22 +5,35 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
lastCount uint64 = 0
|
||||
lastCountRetrievedAt *time.Time = nil
|
||||
lastCountMutex *sync.Mutex = &sync.Mutex{}
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/favicon"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
)
|
||||
|
||||
func init() {
|
||||
app.Use(recover.New())
|
||||
|
||||
app.Use(favicon.New(favicon.Config{
|
||||
Data: faviconData,
|
||||
}))
|
||||
|
||||
if config.Environment == "development" {
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: "*",
|
||||
AllowMethods: "HEAD,OPTIONS,GET",
|
||||
ExposeHeaders: "X-Cache-Hit,X-Cache-Time-Remaining",
|
||||
}))
|
||||
|
||||
app.Use(logger.New(logger.Config{
|
||||
Format: "${time} ${ip}:${port} -> ${status}: ${method} ${path} (${latency})\n",
|
||||
TimeFormat: "2006/01/02 15:04:05",
|
||||
}))
|
||||
}
|
||||
|
||||
app.Get("/ping", PingHandler)
|
||||
app.Get("/favicon.ico", FaviconHandler)
|
||||
app.Get("/count", CountHandler)
|
||||
app.Get("/list", ListHandler)
|
||||
app.Get("/skin/:uuid", SkinHandler)
|
||||
app.Get("/face/:uuid", FaceHandler)
|
||||
@@ -30,11 +43,6 @@ func init() {
|
||||
app.Get("/body/back/:uuid", BackBodyHandler)
|
||||
app.Get("/body/left/:uuid", LeftBodyHandler)
|
||||
app.Get("/body/right/:uuid", RightBodyHandler)
|
||||
app.Use(NotFoundHandler)
|
||||
}
|
||||
|
||||
type CountResponse struct {
|
||||
Count uint64 `json:"count"`
|
||||
}
|
||||
|
||||
// PingHandler is the API handler used for the `/ping` route.
|
||||
@@ -42,48 +50,6 @@ func PingHandler(ctx *fiber.Ctx) error {
|
||||
return ctx.SendStatus(http.StatusOK)
|
||||
}
|
||||
|
||||
// FaviconHandler serves the favicon.ico file to any users that visit the API using a browser.
|
||||
func FaviconHandler(ctx *fiber.Ctx) error {
|
||||
return ctx.Type("ico").Send(favicon)
|
||||
}
|
||||
|
||||
// CountHandler is the API handler used for the `/count` route.
|
||||
func CountHandler(ctx *fiber.Ctx) error {
|
||||
lastCountMutex.Lock()
|
||||
|
||||
defer lastCountMutex.Unlock()
|
||||
|
||||
if lastCountRetrievedAt == nil || time.Since(*lastCountRetrievedAt) > time.Minute*15 {
|
||||
lastCount = 0
|
||||
|
||||
var (
|
||||
keys []string
|
||||
cursor uint64 = 0
|
||||
err error
|
||||
)
|
||||
|
||||
for {
|
||||
keys, cursor, err = r.Scan(cursor, "unique:*", 25)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lastCount += uint64(len(keys))
|
||||
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
lastCountRetrievedAt = PointerOf(time.Now())
|
||||
}
|
||||
|
||||
return ctx.JSON(CountResponse{
|
||||
Count: lastCount,
|
||||
})
|
||||
}
|
||||
|
||||
// ListHandler is the API handler used for the `/list` route.
|
||||
func ListHandler(ctx *fiber.Ctx) error {
|
||||
result := make([]string, 0)
|
||||
@@ -117,6 +83,10 @@ func ListHandler(ctx *fiber.Ctx) error {
|
||||
func SkinHandler(ctx *fiber.Ctx) error {
|
||||
opts := ParseQueryParams(ctx, config.Routes.RawSkin)
|
||||
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
uuid, ok := ParseUUID(ctx.Params("uuid"))
|
||||
|
||||
if !ok {
|
||||
@@ -129,23 +99,27 @@ func SkinHandler(ctx *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := EncodePNG(rawSkin)
|
||||
data, err := EncodeImage(rawSkin, opts)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Download {
|
||||
ctx.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, uuid))
|
||||
ctx.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.%s"`, uuid, opts.Format))
|
||||
}
|
||||
|
||||
return ctx.Type("png").Send(data)
|
||||
return ctx.Type(opts.Format).Send(data)
|
||||
}
|
||||
|
||||
// FaceHandler is the API handler used for the `/face/:uuid` route.
|
||||
func FaceHandler(ctx *fiber.Ctx) error {
|
||||
opts := ParseQueryParams(ctx, config.Routes.Face)
|
||||
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
uuid, ok := ParseUUID(ExtractUUID(ctx))
|
||||
|
||||
if !ok {
|
||||
@@ -170,13 +144,17 @@ func FaceHandler(ctx *fiber.Ctx) error {
|
||||
ctx.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, uuid))
|
||||
}
|
||||
|
||||
return ctx.Type("png").Send(result)
|
||||
return ctx.Type(opts.Format).Send(result)
|
||||
}
|
||||
|
||||
// HeadHandler is the API handler used for the `/head/:uuid` route.
|
||||
func HeadHandler(ctx *fiber.Ctx) error {
|
||||
opts := ParseQueryParams(ctx, config.Routes.Head)
|
||||
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
uuid, ok := ParseUUID(ExtractUUID(ctx))
|
||||
|
||||
if !ok {
|
||||
@@ -201,13 +179,17 @@ func HeadHandler(ctx *fiber.Ctx) error {
|
||||
ctx.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, uuid))
|
||||
}
|
||||
|
||||
return ctx.Type("png").Send(result)
|
||||
return ctx.Type(opts.Format).Send(result)
|
||||
}
|
||||
|
||||
// FullBodyHandler is the API handler used for the `/body/full/:uuid` route.
|
||||
func FullBodyHandler(ctx *fiber.Ctx) error {
|
||||
opts := ParseQueryParams(ctx, config.Routes.FullBody)
|
||||
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
uuid, ok := ParseUUID(ExtractUUID(ctx))
|
||||
|
||||
if !ok {
|
||||
@@ -232,13 +214,17 @@ func FullBodyHandler(ctx *fiber.Ctx) error {
|
||||
ctx.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, uuid))
|
||||
}
|
||||
|
||||
return ctx.Type("png").Send(result)
|
||||
return ctx.Type(opts.Format).Send(result)
|
||||
}
|
||||
|
||||
// FrontBodyHandler is the API handler used for the `/body/front/:uuid` route.
|
||||
func FrontBodyHandler(ctx *fiber.Ctx) error {
|
||||
opts := ParseQueryParams(ctx, config.Routes.FrontBody)
|
||||
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
uuid, ok := ParseUUID(ExtractUUID(ctx))
|
||||
|
||||
if !ok {
|
||||
@@ -263,13 +249,17 @@ func FrontBodyHandler(ctx *fiber.Ctx) error {
|
||||
ctx.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, uuid))
|
||||
}
|
||||
|
||||
return ctx.Type("png").Send(result)
|
||||
return ctx.Type(opts.Format).Send(result)
|
||||
}
|
||||
|
||||
// BackBodyHandler is the API handler used for the `/body/back/:uuid` route.
|
||||
func BackBodyHandler(ctx *fiber.Ctx) error {
|
||||
opts := ParseQueryParams(ctx, config.Routes.BackBody)
|
||||
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
uuid, ok := ParseUUID(ExtractUUID(ctx))
|
||||
|
||||
if !ok {
|
||||
@@ -294,13 +284,17 @@ func BackBodyHandler(ctx *fiber.Ctx) error {
|
||||
ctx.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, uuid))
|
||||
}
|
||||
|
||||
return ctx.Type("png").Send(result)
|
||||
return ctx.Type(opts.Format).Send(result)
|
||||
}
|
||||
|
||||
// LeftBodyHandler is the API handler used for the `/body/left/:uuid` route.
|
||||
func LeftBodyHandler(ctx *fiber.Ctx) error {
|
||||
opts := ParseQueryParams(ctx, config.Routes.LeftBody)
|
||||
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
uuid, ok := ParseUUID(ExtractUUID(ctx))
|
||||
|
||||
if !ok {
|
||||
@@ -325,13 +319,17 @@ func LeftBodyHandler(ctx *fiber.Ctx) error {
|
||||
ctx.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, uuid))
|
||||
}
|
||||
|
||||
return ctx.Type("png").Send(result)
|
||||
return ctx.Type(opts.Format).Send(result)
|
||||
}
|
||||
|
||||
// RightBodyHandler is the API handler used for the `/body/right/:uuid` route.
|
||||
func RightBodyHandler(ctx *fiber.Ctx) error {
|
||||
opts := ParseQueryParams(ctx, config.Routes.RightBody)
|
||||
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
uuid, ok := ParseUUID(ExtractUUID(ctx))
|
||||
|
||||
if !ok {
|
||||
@@ -356,10 +354,5 @@ func RightBodyHandler(ctx *fiber.Ctx) error {
|
||||
ctx.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, uuid))
|
||||
}
|
||||
|
||||
return ctx.Type("png").Send(result)
|
||||
}
|
||||
|
||||
// NotFoundHandler is the API handler used for any requests that do not match an existing route.
|
||||
func NotFoundHandler(ctx *fiber.Ctx) error {
|
||||
return ctx.SendStatus(http.StatusNotFound)
|
||||
return ctx.Type(opts.Format).Send(result)
|
||||
}
|
||||
|
||||
101
src/util.go
101
src/util.go
@@ -8,6 +8,8 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -21,14 +23,21 @@ import (
|
||||
|
||||
var (
|
||||
//go:embed favicon.ico
|
||||
favicon []byte
|
||||
faviconData []byte
|
||||
AllowedFormats []string = []string{
|
||||
"png",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"gif",
|
||||
}
|
||||
)
|
||||
|
||||
// QueryParams is used by most all API routes as options for how the image should be rendered, or how errors should be handled.
|
||||
type QueryParams struct {
|
||||
Scale int `query:"scale"`
|
||||
Download bool `query:"download"`
|
||||
Overlay bool `query:"overlay"`
|
||||
Scale int
|
||||
Download bool
|
||||
Overlay bool
|
||||
Format string
|
||||
}
|
||||
|
||||
// PointerOf returns the value of the first argument as a pointer.
|
||||
@@ -36,6 +45,19 @@ func PointerOf[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
// Contains returns true if the array contains the value.
|
||||
func Contains[T comparable](arr []T, value T) bool {
|
||||
for _, v := range arr {
|
||||
if v != value {
|
||||
continue
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Clamp clamps the input value between the minimum and maximum values.
|
||||
// This method is preferred over `math.Min()` and `math.Max()` to prevent any type coercion between floats and integers.
|
||||
func Clamp[T int | uint | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64 | uint64](value, min, max T) T {
|
||||
@@ -200,31 +222,58 @@ func EncodePNG(img image.Image) ([]byte, error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// EncodeImage encodes the image into the format specified by the query parameters.
|
||||
func EncodeImage(img image.Image, opts *QueryParams) ([]byte, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
switch opts.Format {
|
||||
case "png":
|
||||
{
|
||||
if err := png.Encode(buf, img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case "jpg", "jpeg":
|
||||
{
|
||||
if err := jpeg.Encode(buf, img, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case "gif":
|
||||
{
|
||||
if err := gif.Encode(buf, img, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid format: %s", opts.Format)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// ParseQueryParams parses the query parameters from the request and returns a QueryParams struct, using default values from the provided configuration.
|
||||
func ParseQueryParams(ctx *fiber.Ctx, route RouteConfig) *QueryParams {
|
||||
args := ctx.Context().QueryArgs()
|
||||
format := ctx.Query("format", route.DefaultFormat)
|
||||
|
||||
response := &QueryParams{
|
||||
Scale: route.DefaultScale,
|
||||
Download: route.DefaultDownload,
|
||||
Overlay: route.DefaultOverlay,
|
||||
if !Contains(AllowedFormats, format) {
|
||||
ctx.Status(http.StatusBadRequest).SendString("Invalid 'format' query parameter")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if args.Has("scale") {
|
||||
if scale, err := args.GetUint("scale"); err == nil {
|
||||
response.Scale = Clamp(scale, route.MinScale, route.MaxScale)
|
||||
return &QueryParams{
|
||||
Scale: Clamp(ctx.QueryInt("scale", route.DefaultScale), route.MinScale, route.MaxScale),
|
||||
Download: ctx.QueryBool("download", route.DefaultDownload),
|
||||
Overlay: ctx.QueryBool("overlay", route.DefaultOverlay),
|
||||
Format: ctx.Query("format", route.DefaultFormat),
|
||||
}
|
||||
}
|
||||
|
||||
if args.Has("overlay") {
|
||||
response.Overlay = args.GetBool("overlay")
|
||||
}
|
||||
|
||||
if args.Has("download") {
|
||||
response.Download = args.GetBool("download")
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// GetInstanceID returns the INSTANCE_ID environment variable parsed as an unsigned 16-bit integer.
|
||||
@@ -242,9 +291,9 @@ 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)
|
||||
// SHA256 computes the SHA-256 hash of the input string.
|
||||
func SHA256(value string) string {
|
||||
hash := sha256.Sum256([]byte(value))
|
||||
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user