This commit is contained in:
Mike Schwörer 2023-12-01 09:56:06 +01:00
commit 1f4a477077
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
46 changed files with 14153 additions and 0 deletions

92
.gitignore vendored Normal file
View File

@ -0,0 +1,92 @@
_build
.run-data
_run-data
DOCKER_GIT_INFO
.swaggobin
##############
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**/aws.xml
.idea/**/contentModel.xml
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
.idea/**/gradle.xml
.idea/**/libraries
.idea/**/mongoSettings.xml
.idea/**/sonarlint/
.idea/**/sonarIssues.xml
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
.idea/**/azureSettings.xml
.idea/replstate.xml
.idea/sonarlint/
.idea/httpRequests
.idea/caches/build_file_checksums.ser
.idea/$CACHE_FILE$
.idea/codestream.xml
.idea_modules/
cmake-build-*/
*.iws
out/
atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
*~
.fuse_hidden*
.directory
.Trash-*
.nfs*
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
*.icloud

40
.golangci.yml Normal file
View File

@ -0,0 +1,40 @@
# https://golangci-lint.run/usage/configuration/
run:
go: '1.19'
linters:
enable-all: true
disable:
- golint # deprecated
- exhaustivestruct # deprecated
- deadcode # deprecated
- scopelint # deprecated
- structcheck # deprecated
- varcheck # deprecated
- nosnakecase # deprecated
- maligned # deprecated
- interfacer # deprecated
- ifshort # deprecated
- dupl # (i disagree)
- ireturn # (i disagree)
- wrapcheck # (waiting for bferr)
- goerr113 # (waiting for bferr)
- varnamelen # (too many false-positives)
- gomnd # (i disagree)
- depguard # (not configured)
- gofumpt # (we do not use gofumpt)
- gci # (we do no use gci)
- lll # (i disagree)
- gochecknoglobals # (i disagree)
issues:
exclude-rules:
- path: api/handler/.*.go
linters:
- funlen
linters-settings:
tagalign:
align: true
sort: false

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,11 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

9
.idea/locbunny.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/locbunny.iml" filepath="$PROJECT_DIR$/.idea/locbunny.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

25
Dockerfile Normal file
View File

@ -0,0 +1,25 @@
FROM golang:1-alpine AS builder
RUN apk add --no-cache tzdata ca-certificates openssl make git tar coreutils bash
COPY . /buildsrc
RUN cd /buildsrc && make build
FROM alpine:latest
RUN apk add --no-cache tzdata
COPY --from=builder /buildsrc/_build/bunny_backend /app/server
RUN mkdir /data
WORKDIR /app
EXPOSE 80
CMD ["/app/server"]

95
Makefile Normal file
View File

@ -0,0 +1,95 @@
DOCKER_REPO=registry.blackforestbytes.com
DOCKER_NAME=mikescher/locbunny
NAMESPACE=$(shell git rev-parse --abbrev-ref HEAD)
HASH=$(shell git rev-parse HEAD)
SWAGGO_VERSION=v1.8.12
SWAGGO=github.com/swaggo/swag/cmd/swag@$(SWAGGO_VERSION)
.PHONY: fmt swagger clean run build-docker run-docker-local inspect-docker push-docker lint
build: enums ids swagger fmt
mkdir -p _build
rm -f ./_build/bunny_backend
go build -v -buildvcs=false -o _build/bunny_backend ./cmd/server
enums:
go generate models/enums.go
ids:
go generate models/ids.go
run: build
mkdir -p .run-data
sudo _build/bunny_backend
gow:
# go install github.com/mitranim/gow@latest
gow -c run blackforestbytes.com/locbunny/cmd/server
dgi:
[ ! -f "DOCKER_GIT_INFO" ] || rm DOCKER_GIT_INFO
echo -n "VCSTYPE=" >> DOCKER_GIT_INFO ; echo "git" >> DOCKER_GIT_INFO
echo -n "BRANCH=" >> DOCKER_GIT_INFO ; git rev-parse --abbrev-ref HEAD >> DOCKER_GIT_INFO
echo -n "HASH=" >> DOCKER_GIT_INFO ; git rev-parse HEAD >> DOCKER_GIT_INFO
echo -n "COMMITTIME=" >> DOCKER_GIT_INFO ; git log -1 --format=%cd --date=iso >> DOCKER_GIT_INFO
echo -n "REMOTE=" >> DOCKER_GIT_INFO ; git config --get remote.origin.url >> DOCKER_GIT_INFO
docker: dgi
docker build \
-t "$(DOCKER_NAME):$(HASH)" \
-t "$(DOCKER_NAME):$(NAMESPACE)-latest" \
-t "$(DOCKER_NAME):latest" \
-t "$(DOCKER_REPO)/$(DOCKER_NAME):$(HASH)" \
-t "$(DOCKER_REPO)/$(DOCKER_NAME):$(NAMESPACE)-latest" \
-t "$(DOCKER_REPO)/$(DOCKER_NAME):latest" \
.
swagger-setup:
mkdir -p ".swaggobin"
[ -f ".swaggobin/swag_$(SWAGGO_VERSION)" ] || { GOBIN=/tmp/_swaggo go install $(SWAGGO); cp "/tmp/_swaggo/swag" ".swaggobin/swag_$(SWAGGO_VERSION)"; rm -rf "/tmp/_swaggo"; }
swagger: swagger-setup
".swaggobin/swag_$(SWAGGO_VERSION)" init -generalInfo ./api/router.go --propertyStrategy camelcase --output ./swagger/ --outputTypes "json,yaml" --overridesFile override.swag
run-docker-local: docker
mkdir -p .run-data
docker run --rm \
--init \
--env "CONF_NS=local-docker" \
--volume "$(shell pwd)/.run-data/docker-local:/data" \
--publish "8080:80" \
$(DOCKER_NAME):latest
inspect-docker: docker
mkdir -p .run-data
docker run -ti \
--rm \
--volume "$(shell pwd)/.run-data/docker-inspect:/data" \
$(DOCKER_NAME):latest \
bash
push-docker:
docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):$(HASH)"
docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):$(NAMESPACE)-latest"
docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):latest"
clean:
rm -rf _build/*
rm -rf .run-data/*
git clean -fdx
! which go 2>&1 >> /dev/null || go clean
! which go 2>&1 >> /dev/null || go clean -testcache
fmt: swagger-setup
go fmt ./...
".swaggobin/swag_$(SWAGGO_VERSION)" fmt
.PHONY: test
test:
go test ./test/...
lint:
# curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.2
golangci-lint run ./...

20
_gen/enum-generate.go Normal file
View File

@ -0,0 +1,20 @@
package main
import (
"gogs.mikescher.com/BlackForestBytes/goext/bfcodegen"
"os"
)
func main() {
dest := os.Args[2]
wd, err := os.Getwd()
if err != nil {
panic(err)
}
err = bfcodegen.GenerateEnumSpecs(wd, dest)
if err != nil {
panic(err)
}
}

20
_gen/id-generate.go Normal file
View File

@ -0,0 +1,20 @@
package main
import (
"gogs.mikescher.com/BlackForestBytes/goext/bfcodegen"
"os"
)
func main() {
dest := os.Args[2]
wd, err := os.Getwd()
if err != nil {
panic(err)
}
err = bfcodegen.GenerateIDSpecs(wd, dest)
if err != nil {
panic(err)
}
}

View File

@ -0,0 +1,154 @@
package handler
import (
"bytes"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
"locbunny/logic"
"net/http"
"time"
)
type CommonHandler struct {
app *logic.Application
}
func NewCommonHandler(app *logic.Application) CommonHandler {
return CommonHandler{
app: app,
}
}
type pingResponse struct {
Message string `json:"message"`
Info pingResponseInfo `json:"info"`
}
type pingResponseInfo struct {
Method string `json:"method"`
Request string `json:"request"`
Headers map[string][]string `json:"headers"`
URI string `json:"uri"`
Address string `json:"addr"`
}
// Ping swaggerdoc
//
// @Summary Simple endpoint to test connection (any http method)
// @Tags Common
//
// @Success 200 {object} pingResponse
// @Failure 500 {object} models.APIError
//
// @Router /api/ping [get]
// @Router /api/ping [post]
// @Router /api/ping [put]
// @Router /api/ping [delete]
// @Router /api/ping [patch]
func (h CommonHandler) Ping(pctx ginext.PreContext) ginext.HTTPResponse {
ctx, g, errResp := pctx.Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
buf := new(bytes.Buffer)
_, _ = buf.ReadFrom(g.Request.Body)
resuestBody := buf.String()
return ginext.JSON(http.StatusOK, pingResponse{
Message: "Pong",
Info: pingResponseInfo{
Method: g.Request.Method,
Request: resuestBody,
Headers: g.Request.Header,
URI: g.Request.RequestURI,
Address: g.Request.RemoteAddr,
},
})
}
// Health swaggerdoc
//
// @Summary Server Health-checks
// @Tags Common
//
// @Success 200 {object} handler.Health.response
// @Failure 500 {object} models.APIError
//
// @Router /api/health [get]
func (h CommonHandler) Health(pctx ginext.PreContext) ginext.HTTPResponse {
type response struct {
Status string `json:"status"`
}
ctx, _, errResp := pctx.Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
return ginext.JSON(http.StatusOK, response{Status: "ok"})
}
// Sleep swaggerdoc
//
// @Summary Return 200 after x seconds
// @Tags Common
//
// @Param secs path number true "sleep delay (in seconds)"
//
// @Success 200 {object} handler.Sleep.response
// @Failure 400 {object} models.APIError
// @Failure 500 {object} models.APIError
//
// @Router /api/sleep/:secs [post]
func (h CommonHandler) Sleep(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct {
Seconds float64 `uri:"secs"`
}
type response struct {
Start string `json:"start"`
End string `json:"end"`
Duration float64 `json:"duration"`
}
var u uri
ctx, _, errResp := pctx.URI(&u).Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
t0 := time.Now().Format(time.RFC3339Nano)
time.Sleep(timeext.FromSeconds(u.Seconds))
t1 := time.Now().Format(time.RFC3339Nano)
return ginext.JSON(http.StatusOK, response{
Start: t0,
End: t1,
Duration: u.Seconds,
})
}
func (h CommonHandler) NoRoute(pctx ginext.PreContext) ginext.HTTPResponse {
ctx, g, errResp := pctx.Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
return ginext.JSON(http.StatusNotFound, gin.H{
"": "================ ROUTE NOT FOUND ================",
"FullPath": g.FullPath(),
"Method": g.Request.Method,
"URL": g.Request.URL.String(),
"RequestURI": g.Request.RequestURI,
"Proto": g.Request.Proto,
"Header": g.Request.Header,
"~": "================ ROUTE NOT FOUND ================",
})
}

49
api/handler/webHandler.go Normal file
View File

@ -0,0 +1,49 @@
package handler
import (
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"locbunny/logic"
"locbunny/models"
"net/http"
)
type WebHandler struct {
app *logic.Application
}
func NewWebHandler(app *logic.Application) WebHandler {
return WebHandler{
app: app,
}
}
// ListServer swaggerdoc
//
// @Summary List running server
//
// @Success 200 {object} handler.ListServer.response
// @Failure 400 {object} models.APIError
// @Failure 500 {object} models.APIError
//
// @Router /server [GET]
func (h WebHandler) ListServer(pctx ginext.PreContext) ginext.HTTPResponse {
type response struct {
Server []models.Server `json:"server"`
}
ctx, _, errResp := pctx.Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
srvs, err := h.app.ListServer(ctx)
if err != nil {
return ginext.Error(err)
}
langext.SortBy(srvs, func(v models.Server) int { return v.Port })
return ginext.JSON(http.StatusOK, response{Server: srvs})
}

63
api/router.go Normal file
View File

@ -0,0 +1,63 @@
package api
import (
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
bunny "locbunny"
"locbunny/api/handler"
"locbunny/logic"
"locbunny/swagger"
)
type Router struct {
app *logic.Application
commonHandler handler.CommonHandler
webHandler handler.WebHandler
}
func NewRouter(app *logic.Application) *Router {
return &Router{
app: app,
commonHandler: handler.NewCommonHandler(app),
webHandler: handler.NewWebHandler(app),
}
}
// Init swaggerdocs
//
// @title LocalHostBunny
// @version 1.0
// @host localhost
//
// @BasePath /api/v1/
func (r *Router) Init(e *ginext.GinWrapper) {
api := e.Routes().Group("/api").Group(fmt.Sprintf("/v%d", bunny.APILevel))
// ================ General ================
api.Any("/ping").Handle(r.commonHandler.Ping)
api.GET("/health").Handle(r.commonHandler.Health)
api.POST("/sleep/:secs").Handle(r.commonHandler.Sleep)
// ================ Swagger ================
docs := e.Routes().Group("/documentation")
{
docs.GET("/swagger").Handle(ginext.RedirectTemporary("/documentation/swagger/"))
docs.GET("/swagger/*sub").Handle(swagger.Handle)
}
// ================ API ================
api.GET("/server").Handle(r.webHandler.ListServer)
// ================ ================
if r.app.Config.Custom404 {
e.NoRoute(r.commonHandler.NoRoute)
}
}

32
cmd/server/main.go Normal file
View File

@ -0,0 +1,32 @@
package main
import (
"fmt"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
bunny "locbunny"
"locbunny/api"
"locbunny/logic"
)
func main() {
conf := bunny.Conf
bunny.Init(conf)
log.Info().Msg(fmt.Sprintf("Starting with config-namespace <%s>", conf.Namespace))
app := logic.NewApp()
ginengine := ginext.NewEngine(conf.Cors, conf.GinDebug, true, conf.RequestTimeout)
router := api.NewRouter(app)
appjobs := make([]logic.Job, 0)
app.Init(conf, ginengine, appjobs)
router.Init(ginengine)
app.Run()
}

146
config.go Normal file
View File

@ -0,0 +1,146 @@
package bunny
import (
"github.com/rs/xid"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/confext"
"os"
"time"
)
const APILevel = 1
type Config struct {
Namespace string
GinDebug bool `env:"GINDEBUG"`
ReturnRawErrors bool `env:"RETURNERRORS"`
Custom404 bool `env:"CUSTOM404"`
LogLevel zerolog.Level `env:"LOGLEVEL"`
ServerIP string `env:"IP"`
ServerPort string `env:"PORT"`
RequestTimeout time.Duration `env:"REQUEST_TIMEOUT"`
Cors bool `env:"CORS"`
}
type MailConfig struct {
Host string `env:"HOST"`
Port int `env:"PORT"`
Username string `env:"USERNAME"`
Password string `env:"PASSWORD"`
Sender string `env:"SENDER"`
}
var Conf Config
var configLocHost = func() Config {
return Config{
Namespace: "local",
GinDebug: true,
ServerIP: "0.0.0.0",
ServerPort: "80",
Custom404: true,
ReturnRawErrors: true,
RequestTimeout: 16 * time.Second,
LogLevel: zerolog.DebugLevel,
Cors: true,
}
}
var configLocDocker = func() Config {
return Config{
Namespace: "local-docker",
GinDebug: true,
ServerIP: "0.0.0.0",
ServerPort: "80",
Custom404: true,
ReturnRawErrors: true,
RequestTimeout: 16 * time.Second,
LogLevel: zerolog.DebugLevel,
Cors: true,
}
}
var configDev = func() Config {
return Config{
Namespace: "develop",
GinDebug: true,
ServerIP: "0.0.0.0",
ServerPort: "80",
Custom404: false,
ReturnRawErrors: false,
RequestTimeout: 16 * time.Second,
LogLevel: zerolog.DebugLevel,
Cors: false,
}
}
var configStag = func() Config {
return Config{
Namespace: "staging",
GinDebug: true,
ServerIP: "0.0.0.0",
ServerPort: "80",
Custom404: false,
ReturnRawErrors: false,
RequestTimeout: 16 * time.Second,
LogLevel: zerolog.DebugLevel,
Cors: false,
}
}
var configProd = func() Config {
return Config{
Namespace: "production",
GinDebug: false,
ServerIP: "0.0.0.0",
ServerPort: "80",
Custom404: false,
ReturnRawErrors: false,
RequestTimeout: 16 * time.Second,
LogLevel: zerolog.InfoLevel,
Cors: false,
}
}
var allConfig = map[string]func() Config{
"local-host": configLocHost,
"local-docker": configLocDocker,
"develop": configDev,
"staging": configStag,
"production": configProd,
}
var instID xid.ID
func InstanceID() string {
return instID.String()
}
func getConfig(ns string) (Config, bool) {
if ns == "" {
ns = "local-host"
}
if cfn, ok := allConfig[ns]; ok {
c := cfn()
err := confext.ApplyEnvOverrides("BUNNY_", &c, "_")
if err != nil {
panic(err)
}
return c, true
}
return Config{}, false
}
func init() {
instID = xid.New()
ns := os.Getenv("CONF_NS")
cfg, ok := getConfig(ns)
if !ok {
log.Fatal().Str("ns", ns).Msg("Unknown config-namespace")
}
Conf = cfg
}

42
dgi.go Normal file
View File

@ -0,0 +1,42 @@
package bunny
import (
_ "embed"
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"strings"
)
//go:embed DOCKER_GIT_INFO
var FileDockerGitInfo string
var CommitHash *string
var VCSType *string
var CommitTime *string
var BranchName *string
var RemoteURL *string
func init() {
for _, v := range strings.Split(FileDockerGitInfo, "\n") {
if v == "" {
continue
} else if strings.HasPrefix(v, "VCSTYPE=") {
VCSType = langext.Ptr(v[len("VCSTYPE="):])
fmt.Printf("Found DGI Config: '%s' := '%s'\n", "VCSType", *VCSType)
} else if strings.HasPrefix(v, "BRANCH=") {
BranchName = langext.Ptr(v[len("BRANCH="):])
fmt.Printf("Found DGI Config: '%s' := '%s'\n", "BranchName", *BranchName)
} else if strings.HasPrefix(v, "HASH=") {
CommitHash = langext.Ptr(v[len("HASH="):])
fmt.Printf("Found DGI Config: '%s' := '%s'\n", "CommitHash", *CommitHash)
} else if strings.HasPrefix(v, "COMMITTIME=") {
CommitTime = langext.Ptr(v[len("COMMITTIME="):])
fmt.Printf("Found DGI Config: '%s' := '%s'\n", "CommitTime", *CommitTime)
} else if strings.HasPrefix(v, "REMOTE=") {
RemoteURL = langext.Ptr(v[len("REMOTE="):])
fmt.Printf("Found DGI Config: '%s' := '%s'\n", "RemoteURL", *RemoteURL)
} else {
fmt.Printf("[ERROR] Failed to parse DGI Config '%s'\n", v)
}
}
}

64
errorTypes.go Normal file
View File

@ -0,0 +1,64 @@
package bunny
import (
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
var (
ErrInternal = exerr.TypeInternal
ErrPanic = exerr.TypePanic
ErrWrap = exerr.TypeWrap
ErrNotImplemented = exerr.TypeNotImplemented
ErrBindFailURI = exerr.TypeBindFailURI
ErrBindFailQuery = exerr.TypeBindFailQuery
ErrBindFailJSON = exerr.TypeBindFailJSON
ErrBindFailFormData = exerr.TypeBindFailFormData
ErrUnauthorized = exerr.TypeUnauthorized
ErrAuthFailed = exerr.TypeAuthFailed
ErrDatabaseError = exerr.NewType("DATABASE_ERROR", langext.Ptr(500))
ErrFilesystemError = exerr.NewType("FILESYSTEM_ERROR", langext.Ptr(500))
ErrInvalidStateError = exerr.NewType("INV_STATE_ERROR", langext.Ptr(500))
ErrInvalidRequestParams = exerr.NewType("INVALID_REQUEST_PARAMETER", langext.Ptr(400))
ErrMissingRequestParams = exerr.NewType("MISSING_REQUEST_PARAMETER", langext.Ptr(400))
ErrSelfDelete = exerr.NewType("SELF_DELETE", langext.Ptr(400))
ErrInvalidRefKey = exerr.NewType("INVALID_REF_KEY", langext.Ptr(400))
ErrInvalidMimeType = exerr.NewType("INVALID_MIME_TYPE", langext.Ptr(400))
ErrInvalidBlobType = exerr.NewType("INVALID_BLOB_TYPE", langext.Ptr(400))
ErrInvalidAuthType = exerr.NewType("INVALID_AUTH_TYPE", langext.Ptr(400))
ErrInvalidSubType = exerr.NewType("INVALID_SUB_TYPE", langext.Ptr(400))
ErrPostURLFormat = exerr.NewType("POST_URL_FORMAT", langext.Ptr(400))
ErrEntityNotFound = exerr.NewType("ENTITY_NOT_FOUND", langext.Ptr(400))
ErrInvalidCursorToken = exerr.NewType("INVALID_CURSOR_TOKEN", langext.Ptr(400))
ErrUsernameCollision = exerr.NewType("USERNAME_COLLISION", langext.Ptr(400))
ErrEmailCollision = exerr.NewType("EMAIL_COLLISION", langext.Ptr(400))
ErrPreconditionFailed = exerr.NewType("PRECONDITION_FAILED", langext.Ptr(400))
ErrMissingPermissions = exerr.NewType("MISSING_PERMISSIONS", langext.Ptr(400))
ErrWrongOldPassword = exerr.NewType("WRONG_OLD_PASSWORD", langext.Ptr(400))
ErrWrongSecret = exerr.NewType("WRONG_SECRET", langext.Ptr(400))
ErrMarshalBSON = exerr.NewType("MARSHAL_BSON", langext.Ptr(400))
ErrInvalidJWT = exerr.NewType("INVALID_JWT", langext.Ptr(400))
ErrFAPIError = exerr.NewType("FAPI_ERROR", langext.Ptr(500))
ErrFAPIUnsupported = exerr.NewType("FAPI_UNSUPPORTED", langext.Ptr(500))
ErrSolseitAPIError = exerr.NewType("SOLSEIT_API_ERROR", langext.Ptr(500))
ErrScraper = exerr.NewType("SCRAPER", langext.Ptr(500))
ErrInvalidEnum = exerr.NewType("INVALID_ENUM", langext.Ptr(500))
ErrJob = exerr.NewType("JOB", langext.Ptr(500))
ErrUnmarshalJSON = exerr.NewType("UNMARSHAL_JSON", langext.Ptr(500))
ErrFeatureDisabled = exerr.NewType("FEATURE_DISABLED", langext.Ptr(500))
)

41
go.mod Normal file
View File

@ -0,0 +1,41 @@
module locbunny
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.31.0
go.mongodb.org/mongo-driver v1.12.1
gogs.mikescher.com/BlackForestBytes/goext v0.0.288
)
require (
github.com/bytedance/sonic v1.10.2 // indirect
github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.15.5 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

148
go.sum Normal file
View File

@ -0,0 +1,148 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 h1:BjkPE3785EwPhhyuFkbINB+2a1xATwk8SNDWnJiD41g=
github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5/go.mod h1:jtAfVaU/2cu1+wdSRPWE2c1N2qeAA3K4RH9pYgqwets=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24=
github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE=
go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
gogs.mikescher.com/BlackForestBytes/goext v0.0.288 h1:t4K/rUNGgvP1jSCX1M9fX3I8mD/NOkqCG4DT+zLTEv8=
gogs.mikescher.com/BlackForestBytes/goext v0.0.288/go.mod h1:vzrX73AdSKv6PcqrKZW9yzopTkitDW1T5LknU9oVci0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

54
init.go Normal file
View File

@ -0,0 +1,54 @@
package bunny
import (
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"os"
)
func Init(cfg Config) {
exerr.Init(exerr.ErrorPackageConfigInit{
ZeroLogErrTraces: langext.PTrue,
ZeroLogAllTraces: langext.PTrue,
RecursiveErrors: langext.PTrue,
ExtendedGinOutput: &cfg.ReturnRawErrors,
IncludeMetaInGinOutput: &cfg.ReturnRawErrors,
ExtendGinOutput: func(err *exerr.ExErr, json map[string]any) {
if fapiMsg := err.RecursiveMeta("fapiMessage"); fapiMsg != nil {
json["fapiMessage"] = fapiMsg.ValueString()
}
},
ExtendGinDataOutput: func(err *exerr.ExErr, depth int, json map[string]any) {
if fapiMsg, ok := err.Meta["fapiMessage"]; ok {
json["fapiMessage"] = fapiMsg.ValueString()
}
},
})
cw := zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: "2006-01-02 15:04:05 Z07:00",
}
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
multi := zerolog.MultiLevelWriter(cw)
logger := zerolog.New(multi).With().
Timestamp().
Caller().
Logger()
log.Logger = logger
if cfg.GinDebug {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
zerolog.SetGlobalLevel(cfg.LogLevel)
log.Debug().Msg("Initialized")
}

61
jobs/jobListener.go Normal file
View File

@ -0,0 +1,61 @@
package jobs
import (
"gogs.mikescher.com/BlackForestBytes/goext/rfctime"
"locbunny/logic"
"locbunny/models"
"time"
)
type JobListener struct {
execID models.JobExecutionID
start time.Time
jobName string
logs []models.JobLog
app *logic.Application
}
func NewJobListener(app *logic.Application, id models.JobExecutionID, jobName string) *JobListener {
return &JobListener{
execID: id,
jobName: jobName,
start: time.Now(),
logs: make([]models.JobLog, 0),
app: app,
}
}
func (lstr *JobListener) Log(lvl models.JobLogLevel, logtype string, msg string, extra any) {
logentry := models.JobLog{
JobLogID: models.NewJobLogID(),
JobExecutionID: lstr.execID,
JobName: lstr.jobName,
Type: logtype,
Time: rfctime.NowRFC3339Nano(),
Message: msg,
Level: lvl,
Extra: extra,
}
lstr.logs = append(lstr.logs, logentry)
}
func (lstr *JobListener) LogDebug(logtype string, msg string, extra any) {
lstr.Log(models.JobLogLevelDebug, logtype, msg, extra)
}
func (lstr *JobListener) LogInfo(logtype string, msg string, extra any) {
lstr.Log(models.JobLogLevelInfo, logtype, msg, extra)
}
func (lstr *JobListener) LogWarn(logtype string, msg string, extra any) {
lstr.Log(models.JobLogLevelWarn, logtype, msg, extra)
}
func (lstr *JobListener) LogError(logtype string, msg string, extra any) {
lstr.Log(models.JobLogLevelError, logtype, msg, extra)
}
func (lstr *JobListener) LogFatal(logtype string, msg string, extra any) {
lstr.Log(models.JobLogLevelFatal, logtype, msg, extra)
}

173
jobs/runner.go Normal file
View File

@ -0,0 +1,173 @@
package jobs
import (
"context"
"fmt"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
"gogs.mikescher.com/BlackForestBytes/goext/rfctime"
"gogs.mikescher.com/BlackForestBytes/goext/syncext"
bunny "locbunny"
"locbunny/logic"
"locbunny/models"
"math/rand"
"time"
)
type JobFunction[TData any] func(ctx context.Context, app *logic.Application, lstr *JobListener, data *TData) (int, error)
type JobRunner[TData any] struct {
name string
app *logic.Application
isRunning *syncext.AtomicBool
isStarted bool
interval time.Duration
sigChannel chan string
runTimeout time.Duration
jobFunc JobFunction[TData]
data *TData
}
func NewJobRunner[TData any](app *logic.Application, name string, interval time.Duration, timeout time.Duration, fn JobFunction[TData], data *TData) *JobRunner[TData] {
return &JobRunner[TData]{
app: app,
isRunning: syncext.NewAtomicBool(false),
isStarted: false,
sigChannel: make(chan string),
interval: interval,
runTimeout: timeout,
name: name,
jobFunc: fn,
data: data,
}
}
func (j *JobRunner[TData]) Start() error {
if j.isRunning.Get() {
return exerr.New(bunny.ErrJob, "job already running").Build()
}
if j.isStarted {
return exerr.New(bunny.ErrJob, "job was already started").Build() // re-start after stop is not allowed
}
j.isStarted = true
go j.mainLoop()
return nil
}
func (j *JobRunner[TData]) Stop() {
log.Info().Msg(fmt.Sprintf("Stopping Job [%s]", j.name))
syncext.WriteNonBlocking(j.sigChannel, "stop")
j.isRunning.Wait(false)
log.Info().Msg(fmt.Sprintf("Stopped Job [%s]", j.name))
}
func (j *JobRunner[TData]) Running() bool {
return j.isRunning.Get()
}
func (j *JobRunner[TData]) mainLoop() {
j.isRunning.Set(true)
firstRun := true
for {
interval := j.interval
if firstRun {
// randomize first interval to spread jobs around
perc := mathext.Clamp(rand.Float64(), 0.1, 0.5)
interval = time.Duration(int64(float64(interval) * perc))
}
firstRun = false
signal, okay := syncext.ReadChannelWithTimeout(j.sigChannel, interval)
if okay {
if signal == "stop" {
log.Info().Msg(fmt.Sprintf("Job [%s] received <stop> signal", j.name))
break
} else if signal == "run" {
log.Info().Msg(fmt.Sprintf("Job [%s] received <run> signal", j.name))
continue
} else {
log.Error().Msg(fmt.Sprintf("Received unknown job signal: <%s> in job [%s]", signal, j.name))
}
}
log.Debug().Msg(fmt.Sprintf("Run job [%s]", j.name))
err := j.execute()
if err != nil {
log.Err(err).Msg(fmt.Sprintf("Failed to execute job [%s]: %s", j.name, err.Error()))
}
}
log.Info().Msg(fmt.Sprintf("Job [%s] exiting main-loop", j.name))
j.isRunning.Set(false)
}
func (j *JobRunner[TData]) execute() (err error) {
defer func() {
if rec := recover(); rec != nil {
err = exerr.New(bunny.ErrJob, "Recovered panic in JobRunner::execute").Any("recover", rec).Build()
}
}()
runCtx, cancelRunCtx := context.WithTimeout(context.Background(), j.runTimeout)
defer cancelRunCtx()
jobExec := models.JobExecution{
JobExecutionID: models.NewJobExecutionID(),
JobName: j.name,
StartTime: rfctime.NowRFC3339Nano(),
EndTime: nil,
Changes: 0,
Status: models.JobStatusRunning,
}
lstr := NewJobListener(j.app, jobExec.JobExecutionID, j.name)
lstr.LogInfo("JOB_START", "Job started", nil)
changes, err := langext.RunPanicSafeR2(func() (int, error) { return j.jobFunc(runCtx, j.app, lstr, j.data) })
finCtx, cancelFinCtx := context.WithTimeout(context.Background(), time.Minute)
defer cancelFinCtx()
//goland:noinspection GoTypeAssertionOnErrors
if panicerr, ok := err.(langext.PanicWrappedErr); ok {
jobExec.EndTime = langext.Ptr(rfctime.NowRFC3339Nano())
jobExec.Error = langext.Ptr(panicerr.Error())
jobExec.Status = models.JobStatusFailed
jobExec.Changes = changes
lstr.LogFatal("JOB_PANIC", "Job finished with a panic", langext.H{"msg": panicerr.Error(), "obj": panicerr.ReoveredObj()})
log.Error().Str("panic", panicerr.Error()).Msg(fmt.Sprintf("Job '%s' <%s> finished with panic and %d changes after %f minutes", j.name, jobExec.JobExecutionID, changes, jobExec.Delta().Minutes()))
} else if err != nil {
jobExec.EndTime = langext.Ptr(rfctime.NowRFC3339Nano())
jobExec.Error = langext.Ptr(err.Error())
jobExec.Status = models.JobStatusFailed
jobExec.Changes = changes
lstr.LogFatal("JOB_ERR", "Job finished with an error", langext.H{"msg": err.Error(), "err_obj": exerr.FromError(err).ToAPIJson(false, true, true)})
log.Error().Str("err", err.Error()).Msg(fmt.Sprintf("Job '%s' <%s> finished with an error and %d changes after %f minutes", j.name, jobExec.JobExecutionID, changes, jobExec.Delta().Minutes()))
} else {
jobExec.EndTime = langext.Ptr(rfctime.NowRFC3339Nano())
jobExec.Error = nil
jobExec.Status = models.JobStatusSuccess
jobExec.Changes = changes
lstr.LogInfo("JOB_FINISH", "Job finished", nil)
log.Info().Msg(fmt.Sprintf("Job '%s' <%s> finished successfully with %d changes after %f minutes", j.name, jobExec.JobExecutionID, changes, jobExec.Delta().Minutes()))
}
return nil
}

121
logic/application.go Normal file
View File

@ -0,0 +1,121 @@
package logic
import (
"context"
"github.com/cakturk/go-netstat/netstat"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/syncext"
bunny "locbunny"
"locbunny/models"
"net"
"os"
"os/signal"
"syscall"
"time"
)
type Application struct {
Config bunny.Config
stopChan chan bool
Port string
IsRunning *syncext.AtomicBool
Gin *ginext.GinWrapper
Jobs []Job
}
func NewApp() *Application {
return &Application{
stopChan: make(chan bool),
IsRunning: syncext.NewAtomicBool(false),
}
}
func (app *Application) Init(cfg bunny.Config, g *ginext.GinWrapper, jobs []Job) {
app.Config = cfg
app.Gin = g
app.Jobs = jobs
}
func (app *Application) Stop() {
syncext.WriteNonBlocking(app.stopChan, true)
}
func (app *Application) Run() {
addr := net.JoinHostPort(app.Config.ServerIP, app.Config.ServerPort)
errChan, httpserver := app.Gin.ListenAndServeHTTP(addr, func(port string) {
app.Port = port
app.IsRunning.Set(true)
})
sigstop := make(chan os.Signal, 1)
signal.Notify(sigstop, os.Interrupt, syscall.SIGTERM)
for _, job := range app.Jobs {
err := job.Start()
if err != nil {
log.Fatal().Err(err).Msg("Failed to start job")
}
}
select {
case <-sigstop:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
log.Info().Msg("Stopping HTTP-Server")
err := httpserver.Shutdown(ctx)
if err != nil {
log.Info().Err(err).Msg("Error while stopping the http-server")
} else {
log.Info().Msg("Stopped HTTP-Server")
}
case err := <-errChan:
log.Error().Err(err).Msg("HTTP-Server failed")
case _ = <-app.stopChan:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
log.Info().Msg("Manually stopping HTTP-Server")
err := httpserver.Shutdown(ctx)
if err != nil {
log.Info().Err(err).Msg("Error while stopping the http-server")
} else {
log.Info().Msg("Manually stopped HTTP-Server")
}
}
for _, job := range app.Jobs {
job.Stop()
}
app.IsRunning.Set(false)
}
func (app *Application) ListServer(ctx *ginext.AppContext) ([]models.Server, error) {
socks, err := netstat.TCPSocks(netstat.NoopFilter)
if err != nil {
return nil, err
}
res := make([]models.Server, 0)
for _, sock := range socks {
res = append(res, models.Server{Port: int(sock.LocalAddr.Port)})
}
return res, nil
}

6
logic/jobs.go Normal file
View File

@ -0,0 +1,6 @@
package logic
type Job interface {
Start() error
Stop()
}

7
models/error.go Normal file
View File

@ -0,0 +1,7 @@
package models
type APIError struct {
ErrorCode string `json:"errorcode"`
Message string `json:"message"`
FAPIErrorMessage *string `json:"fapiMessage,omitempty"`
}

21
models/ids.go Normal file
View File

@ -0,0 +1,21 @@
package models
import (
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/bson/primitive"
)
//go:generate go run ../_gen/id-generate.go -- ids_gen.go
type EntityID interface {
ObjID() (primitive.ObjectID, error)
String() string
MarshalBSONValue() (bsontype.Type, []byte, error)
AsAny() AnyID
}
type AnyID string //@id:type
type JobLogID string //@id:type
type JobExecutionID string //@id:type

103
models/ids_gen.go Normal file
View File

@ -0,0 +1,103 @@
// Code generated by id-generate.go DO NOT EDIT.
package models
import "go.mongodb.org/mongo-driver/bson"
import "go.mongodb.org/mongo-driver/bson/bsontype"
import "go.mongodb.org/mongo-driver/bson/primitive"
import "gogs.mikescher.com/BlackForestBytes/goext/exerr"
const ChecksumIDGenerator = "8fa696914bf8d1c1c4b9f80be45d0a9dfbd0fed789856bf84ced2979c789b958" // GoExtVersion: 0.0.288
// ================================ AnyID (ids.go) ================================
func (i AnyID) MarshalBSONValue() (bsontype.Type, []byte, error) {
if objId, err := primitive.ObjectIDFromHex(string(i)); err == nil {
return bson.MarshalValue(objId)
} else {
return 0, nil, exerr.New(exerr.TypeMarshalEntityID, "Failed to marshal AnyID("+i.String()+") to ObjectId").Str("value", string(i)).Type("type", i).Build()
}
}
func (i AnyID) String() string {
return string(i)
}
func (i AnyID) ObjID() (primitive.ObjectID, error) {
return primitive.ObjectIDFromHex(string(i))
}
func (i AnyID) Valid() bool {
_, err := primitive.ObjectIDFromHex(string(i))
return err == nil
}
func (i AnyID) AsAny() AnyID {
return AnyID(i)
}
func NewAnyID() AnyID {
return AnyID(primitive.NewObjectID().Hex())
}
// ================================ JobLogID (ids.go) ================================
func (i JobLogID) MarshalBSONValue() (bsontype.Type, []byte, error) {
if objId, err := primitive.ObjectIDFromHex(string(i)); err == nil {
return bson.MarshalValue(objId)
} else {
return 0, nil, exerr.New(exerr.TypeMarshalEntityID, "Failed to marshal JobLogID("+i.String()+") to ObjectId").Str("value", string(i)).Type("type", i).Build()
}
}
func (i JobLogID) String() string {
return string(i)
}
func (i JobLogID) ObjID() (primitive.ObjectID, error) {
return primitive.ObjectIDFromHex(string(i))
}
func (i JobLogID) Valid() bool {
_, err := primitive.ObjectIDFromHex(string(i))
return err == nil
}
func (i JobLogID) AsAny() AnyID {
return AnyID(i)
}
func NewJobLogID() JobLogID {
return JobLogID(primitive.NewObjectID().Hex())
}
// ================================ JobExecutionID (ids.go) ================================
func (i JobExecutionID) MarshalBSONValue() (bsontype.Type, []byte, error) {
if objId, err := primitive.ObjectIDFromHex(string(i)); err == nil {
return bson.MarshalValue(objId)
} else {
return 0, nil, exerr.New(exerr.TypeMarshalEntityID, "Failed to marshal JobExecutionID("+i.String()+") to ObjectId").Str("value", string(i)).Type("type", i).Build()
}
}
func (i JobExecutionID) String() string {
return string(i)
}
func (i JobExecutionID) ObjID() (primitive.ObjectID, error) {
return primitive.ObjectIDFromHex(string(i))
}
func (i JobExecutionID) Valid() bool {
_, err := primitive.ObjectIDFromHex(string(i))
return err == nil
}
func (i JobExecutionID) AsAny() AnyID {
return AnyID(i)
}
func NewJobExecutionID() JobExecutionID {
return JobExecutionID(primitive.NewObjectID().Hex())
}

40
models/jobExecution.go Normal file
View File

@ -0,0 +1,40 @@
package models
import (
"gogs.mikescher.com/BlackForestBytes/goext/rfctime"
"time"
)
type JobStatus string //@enum:type
const (
JobStatusRunning JobStatus = "RUNNING"
JobStatusSuccess JobStatus = "SUCCESS"
JobStatusFailed JobStatus = "FAILED"
)
type JobExecution struct {
JobExecutionID JobExecutionID `bson:"_id,omitempty" json:"id"`
JobName string `bson:"jobName" json:"jobName"`
StartTime rfctime.RFC3339NanoTime `bson:"startTime" json:"startTime"`
EndTime *rfctime.RFC3339NanoTime `bson:"endTime" json:"endTime"`
Changes int `bson:"changes" json:"changes"`
Status JobStatus `bson:"status" json:"status"`
Error *string `bson:"error" json:"error"`
}
func (u JobExecution) GetID() AnyID {
return u.JobExecutionID.AsAny()
}
func (u JobExecution) GetCreationTime() time.Time {
return u.StartTime.Time()
}
func (u JobExecution) Delta() time.Duration {
if u.EndTime != nil {
return u.EndTime.Sub(u.StartTime)
} else {
return time.Now().Sub(u.StartTime.Time())
}
}

36
models/jobLog.go Normal file
View File

@ -0,0 +1,36 @@
package models
import (
"gogs.mikescher.com/BlackForestBytes/goext/rfctime"
"time"
)
type JobLogLevel string //@enum:type
const (
JobLogLevelDebug JobLogLevel = "DEBUG"
JobLogLevelInfo JobLogLevel = "INFO"
JobLogLevelWarn JobLogLevel = "WARN"
JobLogLevelError JobLogLevel = "ERROR"
JobLogLevelFatal JobLogLevel = "FATAL"
)
type JobLog struct {
JobLogID JobLogID `bson:"_id,omitempty" json:"id"`
JobExecutionID JobExecutionID `bson:"executionId" json:"executionId"`
JobName string `bson:"jobName" json:"jobName"`
Type string `bson:"type" json:"type"`
Time rfctime.RFC3339NanoTime `bson:"time" json:"time"`
Message string `bson:"message" json:"message"`
Level JobLogLevel `bson:"level" json:"level"`
Extra any `bson:"extra" json:"extra"`
Deleted *rfctime.RFC3339NanoTime `bson:"deleted" json:"deleted"`
}
func (u JobLog) GetID() AnyID {
return u.JobLogID.AsAny()
}
func (u JobLog) GetCreationTime() time.Time {
return u.Time.Time()
}

5
models/server.go Normal file
View File

@ -0,0 +1,5 @@
package models
type Server struct {
Port int
}

4
override.swag Normal file
View File

@ -0,0 +1,4 @@
replace rfctime.RFC3339NanoTime string
replace rfctime.SecondsF64 string
replace cryptext.PassHash string
replace enums.EnumMetaValue any

35
swagger/index.html Normal file
View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>API Documentation</title>
<link rel="stylesheet" href="swagger-ui.css" />
<!-- <link rel="stylesheet" href="themes/theme-feeling-blue.css" /> -->
<!-- <link rel="stylesheet" href="themes/theme-flattop.css" /> -->
<!-- <link rel="stylesheet" href="themes/theme-material.css" /> -->
<!-- <link rel="stylesheet" href="themes/theme-monokai.css" /> -->
<!-- <link rel="stylesheet" href="themes/theme-muted.css" /> -->
<!-- <link rel="stylesheet" href="themes/theme-newspaper.css" /> -->
<!-- <link rel="stylesheet" href="themes/theme-outline.css" /> -->
</head>
<body>
<div id="swagger-ui"></div>
<script src="swagger-ui-bundle.js" crossorigin></script>
<script src="swagger-ui-standalone-preset.js" crossorigin></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: './swagger.json',
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
layout: "StandaloneLayout",
defaultModelsExpandDepth: 0,
});
};
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
swagger/swagger-ui.css Normal file

File diff suppressed because one or more lines are too long

73
swagger/swagger.go Normal file
View File

@ -0,0 +1,73 @@
package swagger
import (
"embed"
_ "embed"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"net/http"
"strings"
)
//go:embed *.html
//go:embed *.json
//go:embed *.yaml
//go:embed *.js
//go:embed *.css
//go:embed themes/*
var assets embed.FS
func getAsset(fn string) ([]byte, string, bool) {
data, err := assets.ReadFile(fn)
if err != nil {
return nil, "", false
}
mime := "text/plain"
lowerFN := strings.ToLower(fn)
if strings.HasSuffix(lowerFN, ".html") || strings.HasSuffix(lowerFN, ".htm") {
mime = "text/html"
} else if strings.HasSuffix(lowerFN, ".css") {
mime = "text/css"
} else if strings.HasSuffix(lowerFN, ".js") {
mime = "text/javascript"
} else if strings.HasSuffix(lowerFN, ".json") {
mime = "application/json"
} else if strings.HasSuffix(lowerFN, ".jpeg") || strings.HasSuffix(lowerFN, ".jpg") {
mime = "image/jpeg"
} else if strings.HasSuffix(lowerFN, ".png") {
mime = "image/png"
} else if strings.HasSuffix(lowerFN, ".svg") {
mime = "image/svg+xml"
}
return data, mime, true
}
func Handle(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct {
Filename string `uri:"sub"`
}
var u uri
ctx, _, errResp := pctx.URI(&u).Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
u.Filename = strings.TrimLeft(u.Filename, "/")
if u.Filename == "" {
index, _, _ := getAsset("index.html")
return ginext.Data(http.StatusOK, "text/html", index)
}
if data, mime, ok := getAsset(u.Filename); ok {
return ginext.Data(http.StatusOK, mime, data)
}
return ginext.JSON(http.StatusNotFound, gin.H{"error": "AssetNotFound", "filename": u.Filename})
}

292
swagger/swagger.json Normal file
View File

@ -0,0 +1,292 @@
{
"swagger": "2.0",
"info": {
"title": "LocalHostBunny",
"contact": {},
"version": "1.0"
},
"host": "localhost",
"basePath": "/api/v1/",
"paths": {
"/api/health": {
"get": {
"tags": [
"Common"
],
"summary": "Server Health-checks",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.Health.response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.APIError"
}
}
}
}
},
"/api/ping": {
"get": {
"tags": [
"Common"
],
"summary": "Simple endpoint to test connection (any http method)",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.pingResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.APIError"
}
}
}
},
"put": {
"tags": [
"Common"
],
"summary": "Simple endpoint to test connection (any http method)",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.pingResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.APIError"
}
}
}
},
"post": {
"tags": [
"Common"
],
"summary": "Simple endpoint to test connection (any http method)",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.pingResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.APIError"
}
}
}
},
"delete": {
"tags": [
"Common"
],
"summary": "Simple endpoint to test connection (any http method)",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.pingResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.APIError"
}
}
}
},
"patch": {
"tags": [
"Common"
],
"summary": "Simple endpoint to test connection (any http method)",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.pingResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.APIError"
}
}
}
}
},
"/api/sleep/:secs": {
"post": {
"tags": [
"Common"
],
"summary": "Return 200 after x seconds",
"parameters": [
{
"type": "number",
"description": "sleep delay (in seconds)",
"name": "secs",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.Sleep.response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.APIError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.APIError"
}
}
}
}
},
"/server": {
"get": {
"summary": "List running server",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.ListServer.response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.APIError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.APIError"
}
}
}
}
}
},
"definitions": {
"handler.Health.response": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
},
"handler.ListServer.response": {
"type": "object",
"properties": {
"server": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Server"
}
}
}
},
"handler.Sleep.response": {
"type": "object",
"properties": {
"duration": {
"type": "number"
},
"end": {
"type": "string"
},
"start": {
"type": "string"
}
}
},
"handler.pingResponse": {
"type": "object",
"properties": {
"info": {
"$ref": "#/definitions/handler.pingResponseInfo"
},
"message": {
"type": "string"
}
}
},
"handler.pingResponseInfo": {
"type": "object",
"properties": {
"addr": {
"type": "string"
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"method": {
"type": "string"
},
"request": {
"type": "string"
},
"uri": {
"type": "string"
}
}
},
"models.APIError": {
"type": "object",
"properties": {
"errorcode": {
"type": "string"
},
"fapiMessage": {
"type": "string"
},
"message": {
"type": "string"
}
}
},
"models.Server": {
"type": "object",
"properties": {
"port": {
"type": "integer"
}
}
}
}
}

188
swagger/swagger.yaml Normal file
View File

@ -0,0 +1,188 @@
basePath: /api/v1/
definitions:
handler.Health.response:
properties:
status:
type: string
type: object
handler.ListServer.response:
properties:
server:
items:
$ref: '#/definitions/models.Server'
type: array
type: object
handler.Sleep.response:
properties:
duration:
type: number
end:
type: string
start:
type: string
type: object
handler.pingResponse:
properties:
info:
$ref: '#/definitions/handler.pingResponseInfo'
message:
type: string
type: object
handler.pingResponseInfo:
properties:
addr:
type: string
headers:
additionalProperties:
items:
type: string
type: array
type: object
method:
type: string
request:
type: string
uri:
type: string
type: object
models.APIError:
properties:
errorcode:
type: string
fapiMessage:
type: string
message:
type: string
type: object
models.Server:
properties:
port:
type: integer
type: object
host: localhost
info:
contact: {}
title: LocalHostBunny
version: "1.0"
paths:
/api/health:
get:
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.Health.response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.APIError'
summary: Server Health-checks
tags:
- Common
/api/ping:
delete:
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.pingResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.APIError'
summary: Simple endpoint to test connection (any http method)
tags:
- Common
get:
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.pingResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.APIError'
summary: Simple endpoint to test connection (any http method)
tags:
- Common
patch:
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.pingResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.APIError'
summary: Simple endpoint to test connection (any http method)
tags:
- Common
post:
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.pingResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.APIError'
summary: Simple endpoint to test connection (any http method)
tags:
- Common
put:
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.pingResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.APIError'
summary: Simple endpoint to test connection (any http method)
tags:
- Common
/api/sleep/:secs:
post:
parameters:
- description: sleep delay (in seconds)
in: path
name: secs
required: true
type: number
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.Sleep.response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/models.APIError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.APIError'
summary: Return 200 after x seconds
tags:
- Common
/server:
get:
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.ListServer.response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/models.APIError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.APIError'
summary: List running server
swagger: "2.0"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff