Add format query parameter

This commit is contained in:
Jacob Gunther
2023-08-27 21:38:35 -05:00
parent 4adcd85f4b
commit d11a1c58c5
9 changed files with 204 additions and 169 deletions

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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.

View File

@@ -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"`
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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,33 +222,60 @@ 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.
func GetInstanceID() (uint16, error) {
if instanceID := os.Getenv("INSTANCE_ID"); len(instanceID) > 0 {
@@ -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[:])
}