Initial commit

This commit is contained in:
Miroslav Misek 2025-03-27 09:19:08 +01:00
commit 57eaeea310
15 changed files with 763 additions and 0 deletions

8
.idea/maf-webserver.iml generated Normal file
View File

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

8
.idea/modules.xml generated 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/maf-webserver.iml" filepath="$PROJECT_DIR$/.idea/maf-webserver.iml" />
</modules>
</component>
</project>

9
.idea/workspace.xml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectViewState">
<option name="autoscrollFromSource" value="true" />
<option name="autoscrollToSource" value="true" />
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
</project>

5
common.go Normal file
View File

@ -0,0 +1,5 @@
package webserver
import "github.com/labstack/echo/v4"
type MiddlewareFunc = echo.MiddlewareFunc

15
config.go Normal file
View File

@ -0,0 +1,15 @@
package webserver
type Config struct {
Port uint16 `yaml:"port" envconfig:"PORT"`
BaseUrl string `yaml:"baseUrl" envconfig:"BASE_URL"`
ReloadTemplates bool `yaml:"reloadTemplates" envconfig:"RELOAD_TEMPLATES"`
}
func NewConfig() *Config {
return &Config{
Port: 8080,
BaseUrl: "",
ReloadTemplates: false,
}
}

149
context.go Normal file
View File

@ -0,0 +1,149 @@
package webserver
import (
"errors"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"log/slog"
"net/http"
)
type HandlerFunc func(c *Context) error
type Context struct {
echo.Context
csrfConfig *middleware.CSRFConfig
}
func CustomContext(csrfConfig *middleware.CSRFConfig) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cc := &Context{
Context: c,
csrfConfig: csrfConfig,
}
return next(cc)
}
}
}
func (c *Context) View(code int, filename string, obj interface{}) error {
if mapContext, ok := obj.(map[string]interface{}); ok {
viewData := mapContext
viewData["request"] = c.Request()
viewData["csrf"] = c.CSRF()
obj = viewData
}
return c.Context.Render(code, filename, obj)
}
func (c *Context) DtoError(err error) error {
switch err.(type) {
case *NotFoundError:
return echo.NewHTTPError(http.StatusNotFound, "Not found")
case *InternalError:
return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error")
case *InvalidRequestError:
return echo.NewHTTPError(http.StatusBadRequest, "Bad request")
default:
return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error")
}
}
func (c *Context) NotFound() error {
return echo.NewHTTPError(http.StatusNotFound)
}
func (c *Context) InternalServerError(err ...error) error {
if err != nil && len(err) > 0 {
slog.Error("", err)
}
return echo.NewHTTPError(http.StatusInternalServerError)
}
func Handle(handler HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return handler(c.(*Context))
}
}
func (c *Context) CSRF() string {
return c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
}
func (c *Context) GetFormData() (map[string]string, error) {
var err error
ret := make(map[string]string)
//c.MultipartForm()
contentType := c.Request().Header.Get("Content-Type")
if contentType == "multipart/form-data" {
err = c.Request().ParseMultipartForm(4 * 1024 * 1024)
} else if contentType == "application/x-www-form-urlencoded" {
err = c.Request().ParseForm()
} else {
err = errors.New("unsupported content type")
}
if err != nil {
return nil, err
}
for k, v := range c.Request().PostForm {
ret[k] = v[len(v)-1]
}
return ret, nil
}
/*
func requestQueryToFilter(c *Context, params map[string]string) (i2bolt.Filter, map[string]string) {
var subFilters []i2bolt.Filter
filterParams := make(map[string]string)
query := c.Request().URL.Query()
for param, fieldMethod := range params {
if v, ok := query[param]; ok {
value := strings.TrimSpace(v[0])
if value != "" {
parts := strings.Split(fieldMethod, "__")
fieldName := parts[0]
method := ""
if len(parts) > 1 {
method = parts[1]
}
var valueIntf interface{}
valueIntf = value
if method == "is" {
valueIntf = value == "yes"
}
fieldNames := strings.Split(fieldName, "|")
for _, name := range fieldNames {
subFilters = append(subFilters, i2bolt.Q(name, method, valueIntf))
filterParams[param] = value
}
} else {
filterParams[param] = ""
}
}
}
var filter i2bolt.Filter
if len(subFilters) > 0 {
filter = i2bolt.And(subFilters...)
}
return filter, filterParams
}
*/

70
dto.go Normal file
View File

@ -0,0 +1,70 @@
package webserver
import "fmt"
type GateError struct {
Message string
Err error
}
func (e *GateError) Error() string {
if e.Message != "" && e.Err != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
} else if e.Message != "" {
return e.Message
} else {
return e.Err.Error()
}
}
type InputError struct {
GateError
}
func NewInputError(message string, err error) *InputError {
return &InputError{
GateError{
Message: message,
Err: err,
},
}
}
type InternalError struct {
GateError
}
func NewInternalError(message string, err error) *InternalError {
return &InternalError{
GateError{
Message: message,
Err: err,
},
}
}
type NotFoundError struct {
GateError
}
func NewNotFoundError(message string, err error) *NotFoundError {
return &NotFoundError{
GateError{
Message: message,
Err: err,
},
}
}
type InvalidRequestError struct {
GateError
}
func NewInvalidRequestError(message string, err error) *InvalidRequestError {
return &InvalidRequestError{
GateError{
Message: message,
Err: err,
},
}
}

30
go.mod Normal file
View File

@ -0,0 +1,30 @@
module netgarden.dev/netgarden/maf-webserver
go 1.24.1
replace netgarden.dev/netgarden/maf => ../maf
replace netgarden.dev/netgarden/mergefs => ../mergefs
require (
github.com/flosch/pongo2/v6 v6.0.0
github.com/labstack/echo/v4 v4.13.3
netgarden.dev/netgarden/maf v0.0.0-00010101000000-000000000000
netgarden.dev/netgarden/mergefs v0.0.0-00010101000000-000000000000
)
require (
github.com/elliotchance/orderedmap v1.6.0 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.8.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

50
go.sum Normal file
View File

@ -0,0 +1,50 @@
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/elliotchance/orderedmap v1.6.0 h1:xjn+kbbKXeDq6v9RVE+WYwRbYfAZKvlWfcJNxM8pvEw=
github.com/elliotchance/orderedmap v1.6.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=
github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU=
github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
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=

28
group.go Normal file
View File

@ -0,0 +1,28 @@
package webserver
import (
"github.com/labstack/echo/v4"
"strings"
)
type echoGroup = echo.Group
type Group struct {
*echoGroup
}
func (g *Group) Group(prefix string, middleware ...MiddlewareFunc) *Group {
return &Group{
g.echoGroup.Group(prefix, middleware...),
}
}
func (g *Group) AddMany(methods string, path string, handler echo.HandlerFunc) {
for _, method := range strings.Split(methods, " ") {
method = strings.TrimSpace(method)
if method != "" {
g.Add(method, path, handler)
}
}
}

36
interfaces.go Normal file
View File

@ -0,0 +1,36 @@
package webserver
import (
"github.com/flosch/pongo2/v6"
"io/fs"
"netgarden.dev/netgarden/maf"
)
type Controller interface {
Register(app *Group)
}
type WebModule interface {
maf.Module
GetWebControllers() []Controller
}
type WebModuleStaticFSProvider interface {
WebModule
GetWebStaticFS() fs.FS
}
type WebModuleTemplatesFSProvider interface {
WebModule
GetWebTemplatesFS() fs.FS
}
type WebModuleTemplateTagsProvider interface {
WebModule
GetWebTemplateTags(renderer *Renderer) map[string]pongo2.TagParser
}
type WebModuleTemplateFiltersProvider interface {
WebModule
GetWebTemplateFilters() map[string]pongo2.FilterFunction
}

144
module.go Normal file
View File

@ -0,0 +1,144 @@
package webserver
import (
"github.com/flosch/pongo2/v6"
"io/fs"
"netgarden.dev/netgarden/maf"
"netgarden.dev/netgarden/mergefs"
)
func NewModule(moduleConfig *ModuleConfig) *Module {
return &Module{
moduleConfig: moduleConfig,
}
}
type Module struct {
moduleConfig *ModuleConfig
manager *maf.Manager
config *Config
server *Server
}
func (m *Module) GetID() string {
return "webserver"
}
func (m *Module) GetName() string {
return "webserver"
}
func (m *Module) SetManager(manager *maf.Manager) {
m.manager = manager
}
func (m *Module) CreateConfig() interface{} {
return NewConfig()
}
func (m *Module) SetConfig(cfg interface{}) {
m.config = cfg.(*Config)
}
func (m *Module) GetConfig() *Config {
return m.config
}
func (m *Module) Initialize() error {
return nil
}
func (m *Module) PreStart() error {
m.server = NewServer(m)
for _, module := range m.manager.GetModulesList() {
webModule, ok := module.(WebModule)
if !ok {
continue
}
if tagsProvider, ok := module.(WebModuleTemplateTagsProvider); ok {
for name, tag := range tagsProvider.GetWebTemplateTags(m.server.GetRenderer()) {
err := pongo2.RegisterTag(name, tag)
if err != nil {
return err
}
}
}
if filtersProvider, ok := module.(WebModuleTemplateFiltersProvider); ok {
for name, filter := range filtersProvider.GetWebTemplateFilters() {
err := pongo2.RegisterFilter(name, filter)
if err != nil {
return err
}
}
}
routerGroup := m.server.GetGroup()
controllers := webModule.GetWebControllers()
for _, controller := range controllers {
controller.Register(routerGroup)
}
}
return nil
}
func (m *Module) Start() error {
return m.server.Start()
}
func (m *Module) Stop() error {
if m.server == nil {
return nil
}
return m.server.Stop()
}
func (m *Module) getStaticFS() fs.FS {
fsList := make([]fs.FS, 0)
for _, module := range m.manager.GetModulesList() {
webModule, ok := module.(WebModuleStaticFSProvider)
if !ok {
continue
}
tplFS := webModule.GetWebStaticFS()
if tplFS == nil {
continue
}
fsList = append(fsList, tplFS)
}
return mergefs.Merge(fsList...)
}
func (m *Module) getTemplatesFS() fs.FS {
fsList := make([]fs.FS, 0)
for _, module := range m.manager.GetModulesList() {
webModule, ok := module.(WebModuleTemplatesFSProvider)
if !ok {
continue
}
tplFS := webModule.GetWebTemplatesFS()
if tplFS == nil {
continue
}
fsList = append(fsList, tplFS)
}
return mergefs.Merge(fsList...)
}

7
module_config.go Normal file
View File

@ -0,0 +1,7 @@
package webserver
type MiddlewareFuncProvider = func() MiddlewareFunc
type ModuleConfig struct {
Middlewares []MiddlewareFuncProvider
}

89
renderer.go Normal file
View File

@ -0,0 +1,89 @@
package webserver
import (
"bytes"
"errors"
"fmt"
"github.com/flosch/pongo2/v6"
"github.com/labstack/echo/v4"
"io"
"io/fs"
)
type Renderer struct {
fs fs.FS
templates *pongo2.TemplateSet
}
// NewRenderer creates a new Renderer struct.
func NewRenderer(fs fs.FS) *Renderer {
r := &Renderer{
fs: fs,
}
templates := pongo2.NewSet("templates", r)
r.templates = templates
return r
}
// Abs returns absolute path to file requested.
// Search path is configured in AddDirectory method.
// And default directory is "./templates".
func (p *Renderer) Abs(base, name string) string {
return name
}
// Get reads the path's content from your local filesystem.
func (p *Renderer) Get(path string) (io.Reader, error) {
f, err := p.fs.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return nil, err
}
return bytes.NewReader(data), nil
}
// RegisterTag registers a custom tag.
// It calls pongo2.RegisterTag method.
func (p *Renderer) RegisterTag(name string, parserFunc pongo2.TagParser) {
pongo2.RegisterTag(name, parserFunc)
}
// RegisterFilter registers a custom filter.
// It calls pongo2.RegisterFilter method.
func (p *Renderer) RegisterFilter(name string, fn pongo2.FilterFunction) {
pongo2.RegisterFilter(name, fn)
}
// SetDebug sets debug mode to the template set.
// See pongo2.TemplateSet.Debug for more information.
func (p *Renderer) SetDebug(v bool) {
p.templates.Debug = v
}
// Render renders the view.
// Many other times, this is called in your echo handler functions.
func (p *Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
tmpl, err := p.templates.FromCache(name)
if err != nil {
return err
}
if tmpl == nil {
return fmt.Errorf("template '%s' not found", name)
}
d, ok := data.(map[string]interface{})
if !ok {
return errors.New("Incorrect data format. Should be map[string]interface{}")
}
return tmpl.ExecuteWriter(d, w)
}

115
server.go Normal file
View File

@ -0,0 +1,115 @@
package webserver
import (
"context"
"fmt"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"log/slog"
"net"
"net/http"
"syscall"
"time"
)
type Server struct {
module *Module
listener *net.Listener
csrfConfig *middleware.CSRFConfig
router *echo.Echo
server *http.Server
}
func NewServer(module *Module) *Server {
s := &Server{
module: module,
}
s.init()
return s
}
func (s *Server) init() {
s.csrfConfig = &middleware.CSRFConfig{
TokenLookup: "form:_csrf,query:csrf,header:X-CSRF-Token",
}
s.router = echo.New()
//s.router.Pre(middleware.RemoveTrailingSlash())
s.router.Use(middleware.Logger())
s.router.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
LogLevel: 4,
}))
s.router.Use(middleware.CSRFWithConfig(*s.csrfConfig))
s.router.Use(CustomContext(s.csrfConfig))
if s.module.moduleConfig != nil && s.module.moduleConfig.Middlewares != nil {
for _, m := range s.module.moduleConfig.Middlewares {
s.router.Use(m())
}
}
tplFS := s.module.getTemplatesFS()
s.router.Renderer = NewRenderer(tplFS)
s.router.StaticFS("/static", s.module.getStaticFS())
}
func (s *Server) Start() error {
config := &net.ListenConfig{Control: s.reusePort}
listener, err := config.Listen(
context.Background(),
"tcp",
fmt.Sprintf(":%d", s.module.config.Port),
)
if err != nil {
return err
}
s.listener = &listener
s.server = &http.Server{
Addr: fmt.Sprintf(":%d", s.module.config.Port),
Handler: s.router,
}
go s.server.Serve(listener)
return nil
}
func (s *Server) Stop() error {
if s.server == nil {
return nil
}
timeout := 10 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return s.server.Shutdown(ctx)
}
func (s *Server) GetGroup() *Group {
return &Group{
s.router.Group("/"),
}
}
func (s *Server) GetRenderer() *Renderer {
return s.router.Renderer.(*Renderer)
}
func (s *Server) reusePort(network, address string, conn syscall.RawConn) error {
return conn.Control(func(descriptor uintptr) {
err := syscall.SetsockoptInt(int(descriptor), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
if err != nil {
slog.Error("error during setting reuseport", err)
}
})
}