MVP
This commit is contained in:
commit
1f4a477077
92
.gitignore
vendored
Normal file
92
.gitignore
vendored
Normal 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
40
.golangci.yml
Normal 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
8
.idea/.gitignore
vendored
Normal 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
|
11
.idea/inspectionProfiles/Project_Default.xml
Normal file
11
.idea/inspectionProfiles/Project_Default.xml
Normal 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
9
.idea/locbunny.iml
Normal 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
8
.idea/modules.xml
Normal 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
6
.idea/vcs.xml
Normal 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
25
Dockerfile
Normal 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
95
Makefile
Normal 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
20
_gen/enum-generate.go
Normal 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
20
_gen/id-generate.go
Normal 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)
|
||||
}
|
||||
}
|
154
api/handler/commonHandler.go
Normal file
154
api/handler/commonHandler.go
Normal 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
49
api/handler/webHandler.go
Normal 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
63
api/router.go
Normal 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
32
cmd/server/main.go
Normal 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
146
config.go
Normal 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
42
dgi.go
Normal 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
64
errorTypes.go
Normal 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
41
go.mod
Normal 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
148
go.sum
Normal 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
54
init.go
Normal 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
61
jobs/jobListener.go
Normal 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
173
jobs/runner.go
Normal 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
121
logic/application.go
Normal 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
6
logic/jobs.go
Normal file
@ -0,0 +1,6 @@
|
||||
package logic
|
||||
|
||||
type Job interface {
|
||||
Start() error
|
||||
Stop()
|
||||
}
|
7
models/error.go
Normal file
7
models/error.go
Normal 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
21
models/ids.go
Normal 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
103
models/ids_gen.go
Normal 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
40
models/jobExecution.go
Normal 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
36
models/jobLog.go
Normal 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
5
models/server.go
Normal file
@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
type Server struct {
|
||||
Port int
|
||||
}
|
4
override.swag
Normal file
4
override.swag
Normal 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
35
swagger/index.html
Normal 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>
|
3
swagger/swagger-ui-bundle.js
Normal file
3
swagger/swagger-ui-bundle.js
Normal file
File diff suppressed because one or more lines are too long
3
swagger/swagger-ui-standalone-preset.js
Normal file
3
swagger/swagger-ui-standalone-preset.js
Normal file
File diff suppressed because one or more lines are too long
4
swagger/swagger-ui.css
Normal file
4
swagger/swagger-ui.css
Normal file
File diff suppressed because one or more lines are too long
73
swagger/swagger.go
Normal file
73
swagger/swagger.go
Normal 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
292
swagger/swagger.json
Normal 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
188
swagger/swagger.yaml
Normal 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"
|
1672
swagger/themes/theme-feeling-blue.css
Normal file
1672
swagger/themes/theme-feeling-blue.css
Normal file
File diff suppressed because it is too large
Load Diff
1672
swagger/themes/theme-flattop.css
Normal file
1672
swagger/themes/theme-flattop.css
Normal file
File diff suppressed because it is too large
Load Diff
1719
swagger/themes/theme-material.css
Normal file
1719
swagger/themes/theme-material.css
Normal file
File diff suppressed because it is too large
Load Diff
1792
swagger/themes/theme-monokai.css
Normal file
1792
swagger/themes/theme-monokai.css
Normal file
File diff suppressed because it is too large
Load Diff
1673
swagger/themes/theme-muted.css
Normal file
1673
swagger/themes/theme-muted.css
Normal file
File diff suppressed because it is too large
Load Diff
1671
swagger/themes/theme-newspaper.css
Normal file
1671
swagger/themes/theme-newspaper.css
Normal file
File diff suppressed because it is too large
Load Diff
1652
swagger/themes/theme-outline.css
Normal file
1652
swagger/themes/theme-outline.css
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user