Compare commits

...

14 Commits
v0.0.7 ... main

Author SHA1 Message Date
Yavolte fb585fabe6 use jwt claims 2025-06-30 11:25:55 +08:00
Yavolte 29d609ce0a fix cache bugs 2025-06-30 11:11:34 +08:00
Yavolte b0d6e3423d add gzip support 2025-06-24 15:51:18 +08:00
fcl 4792b21857 updat README 2025-06-23 17:53:41 +08:00
fcl 0d0477d32c add readme 2025-06-23 17:36:28 +08:00
fcl c027ef22bc fix log 2025-06-23 17:20:10 +08:00
Yavolte 908931bc01 add query 2025-06-23 17:00:36 +08:00
Yavolte f79d3f28e2 add httpclient 2025-06-19 10:01:05 +08:00
Yavolte 9dc6735a61 remove tools 2025-06-18 21:26:59 +08:00
Yavolte 673d22fcff add redis cache 2025-06-18 17:47:04 +08:00
Yavolte 4dc5e64dde add jwt custom validate supported 2025-06-18 16:02:03 +08:00
Yavolte 805ad282f5 add jwt custom validate supported 2025-06-18 15:51:11 +08:00
Yavolte 0bc3f9d170 add transport address from env variable 2025-06-17 18:39:40 +08:00
Yavolte f5687a0094 update cache instance and update rest proto 2025-06-17 17:16:11 +08:00
32 changed files with 1377 additions and 551 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ _posts
.vscode .vscode
vendor vendor
cmd/ cmd/
tools/
third_party/ third_party/
# Go.gitignore # Go.gitignore

View File

@ -4,5 +4,99 @@
# 环境变量
| 环境变量 | 描述 |
| --- | --- |
| AEUS_DEBUG | 是否开启debug模式 |
| HTTP_PORT | http服务端口 |
| GRPC_PORT | grpc服务端口 |
| CLI_PORT | cli服务端口 |
# 快速开始 # 快速开始
## 创建一个项目
创建项目可以使用`aeus`命令行工具进行生成:
```
aeus new github.com/your-username/your-project-name
```
如果需要创建一个带管理后台的应用, 可以使用`--admin`参数:
```
aeus new github.com/your-username/your-project-name --admin
```
## 生成`Proto`文件
服务使用`proto3`作为通信协议,因此需要生成`Proto`文件。
```
make proto
```
清理生成的文件使用:
```
make proto-clean
```
## 编译项目
编译项目可以使用`make`命令进行编译:
```
make build
```
# 目录结构
```
├── api
│ └── v1
├── cmd
│ ├── main.go
├── config
│ ├── config.go
│ └── config.yaml
├── deploy
│ └── docker
├── go.mod
├── go.sum
├── internal
│ ├── models
│ ├── scope
│ ├── service
├── Makefile
├── README.md
├── third_party
│ ├── aeus
│ ├── errors
│ ├── google
│ ├── openapi
│ ├── README.md
│ └── validate
├── vendor
├── version
│ └── version.go
├── web
└── webhook.yaml
```
| 目录 | 描述 |
| --- | --- |
| api | api定义目录 |
| cmd | 启动命令目录 |
| config | 配置目录 |
| deploy | 部署目录 |
| internal | 内部文件目录 |
| internal.service | 服务定义目录 |
| internal.models | 模型定义目录 |
| internal.scope | 服务scope定义目录,主要有全局的变量(比如DB,Redis等) |
| third_party | 第三方proto文件目录 |
| web | 前端资源目录 |

13
app.go
View File

@ -6,6 +6,7 @@ import (
"os/signal" "os/signal"
"reflect" "reflect"
"runtime" "runtime"
"strconv"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time" "time"
@ -35,12 +36,19 @@ func (s *Service) Name() string {
} }
func (s *Service) Debug() bool { func (s *Service) Debug() bool {
if s.opts != nil {
return s.opts.debug
}
return false return false
} }
func (s *Service) Version() string { func (s *Service) Version() string {
return s.opts.version if s.opts != nil {
return s.opts.version
}
return ""
} }
func (s *Service) Metadata() map[string]string { func (s *Service) Metadata() map[string]string {
if s.service == nil { if s.service == nil {
return nil return nil
@ -114,7 +122,7 @@ func (s *Service) injectVars(v any) {
} }
func (s *Service) preStart(ctx context.Context) (err error) { func (s *Service) preStart(ctx context.Context) (err error) {
s.Logger().Info(s.ctx, "starting") s.Logger().Info(ctx, "starting")
for _, ptr := range s.opts.servers { for _, ptr := range s.opts.servers {
s.refValues = append(s.refValues, reflect.ValueOf(ptr)) s.refValues = append(s.refValues, reflect.ValueOf(ptr))
} }
@ -255,6 +263,7 @@ func New(cbs ...Option) *Service {
registrarTimeout: time.Second * 30, registrarTimeout: time.Second * 30,
}, },
} }
s.opts.debug, _ = strconv.ParseBool("AEUS_DEBUG")
s.opts.metadata = make(map[string]string) s.opts.metadata = make(map[string]string)
for _, cb := range cbs { for _, cb := range cbs {
cb(s.opts) cb(s.opts)

3
go.mod
View File

@ -11,6 +11,7 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/mattn/go-runewidth v0.0.16 github.com/mattn/go-runewidth v0.0.16
github.com/peterh/liner v1.2.2 github.com/peterh/liner v1.2.2
github.com/redis/go-redis/v9 v9.10.0
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
go.etcd.io/etcd/api/v3 v3.6.0 go.etcd.io/etcd/api/v3 v3.6.0
go.etcd.io/etcd/client/v3 v3.6.0 go.etcd.io/etcd/client/v3 v3.6.0
@ -24,10 +25,12 @@ require (
require ( require (
github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/iasm v0.2.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect

10
go.sum
View File

@ -1,7 +1,13 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
@ -14,6 +20,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
@ -86,6 +94,8 @@ github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=

View File

@ -2,6 +2,7 @@ package auth
import ( import (
"context" "context"
"reflect"
"strings" "strings"
"git.nobla.cn/golang/aeus/metadata" "git.nobla.cn/golang/aeus/metadata"
@ -29,19 +30,15 @@ const (
type Option func(*options) type Option func(*options)
// Parser is a jwt parser type Validate interface {
type options struct { Validate(ctx context.Context, token string) error
allows []string
claims func() jwt.Claims
} }
// WithClaims with customer claim // Parser is a jwt parser
// If you use it in Server, f needs to return a new jwt.Claims object each time to avoid concurrent write problems type options struct {
// If you use it in Client, f only needs to return a single object to provide performance allows []string
func WithClaims(f func() jwt.Claims) Option { claims reflect.Type
return func(o *options) { validate Validate
o.claims = f
}
} }
// WithAllow with allow path // WithAllow with allow path
@ -50,27 +47,52 @@ func WithAllow(paths ...string) Option {
if o.allows == nil { if o.allows == nil {
o.allows = make([]string, 0, 16) o.allows = make([]string, 0, 16)
} }
o.allows = append(o.allows, paths...) for _, s := range paths {
s = strings.TrimSpace(s)
if len(s) == 0 {
continue
}
o.allows = append(o.allows, s)
}
}
}
func WithClaims(claims any) Option {
return func(o *options) {
if tv, ok := claims.(reflect.Type); ok {
o.claims = tv
} else {
o.claims = reflect.TypeOf(claims)
if o.claims.Kind() == reflect.Ptr {
o.claims = o.claims.Elem()
}
}
}
}
func WithValidate(fn Validate) Option {
return func(o *options) {
o.validate = fn
} }
} }
// isAllowed check if the path is allowed // isAllowed check if the path is allowed
func isAllowed(uripath string, allows []string) bool { func isAllowed(uripath string, allows []string) bool {
for _, str := range allows { for _, pattern := range allows {
n := len(str) n := len(pattern)
if n == 0 { if pattern == uripath {
continue return true
} }
if n > 1 && str[n-1] == '*' { if pattern == "*" {
if strings.HasPrefix(uripath, str[:n-1]) { return true
}
if n > 1 && pattern[n-1] == '*' {
if strings.HasPrefix(uripath, pattern[:n-1]) {
return true return true
} }
} }
if str == uripath {
return true
}
} }
return true return false
} }
// JWT auth middleware // JWT auth middleware
@ -90,21 +112,27 @@ func JWT(keyFunc jwt.Keyfunc, cbs ...Option) middleware.Middleware {
} }
} }
} }
authorizationValue, ok := md.Get(authorizationKey) token, ok := md.Get(authorizationKey)
if !ok { if !ok {
return errors.ErrAccessDenied return errors.ErrAccessDenied
} }
if !strings.HasPrefix(authorizationValue, bearerWord) { if opts.validate != nil {
return errors.ErrAccessDenied if err = opts.validate.Validate(ctx, token); err != nil {
return err
}
} }
token, _ = strings.CutPrefix(token, bearerWord)
var ( var (
ti *jwt.Token ti *jwt.Token
) )
authorizationToken := strings.TrimSpace(strings.TrimPrefix(authorizationValue, bearerWord)) token = strings.TrimSpace(token)
if opts.claims != nil { if opts.claims != nil {
ti, err = jwt.ParseWithClaims(authorizationToken, opts.claims(), keyFunc) if claims, ok := reflect.New(opts.claims).Interface().(jwt.Claims); ok {
} else { ti, err = jwt.ParseWithClaims(token, claims, keyFunc)
ti, err = jwt.Parse(authorizationToken, keyFunc) }
}
if ti == nil {
ti, err = jwt.Parse(token, keyFunc)
} }
if err != nil { if err != nil {
if errors.Is(err, jwt.ErrTokenMalformed) || errors.Is(err, jwt.ErrTokenUnverifiable) { if errors.Is(err, jwt.ErrTokenMalformed) || errors.Is(err, jwt.ErrTokenUnverifiable) {

View File

@ -2,6 +2,7 @@ package aeus
import ( import (
"context" "context"
"maps"
"time" "time"
"git.nobla.cn/golang/aeus/pkg/logger" "git.nobla.cn/golang/aeus/pkg/logger"
@ -18,6 +19,7 @@ type options struct {
servers []Server servers []Server
endpoints []string endpoints []string
scope Scope scope Scope
debug bool
registrarTimeout time.Duration registrarTimeout time.Duration
registry registry.Registry registry registry.Registry
serviceLoader ServiceLoader serviceLoader ServiceLoader
@ -41,9 +43,7 @@ func WithMetadata(metadata map[string]string) Option {
if o.metadata == nil { if o.metadata == nil {
o.metadata = make(map[string]string) o.metadata = make(map[string]string)
} }
for k, v := range metadata { maps.Copy(o.metadata, metadata)
o.metadata[k] = v
}
} }
} }
@ -71,6 +71,12 @@ func WithScope(scope Scope) Option {
} }
} }
func WithDebug(debug bool) Option {
return func(o *options) {
o.debug = debug
}
}
func WithServiceLoader(loader ServiceLoader) Option { func WithServiceLoader(loader ServiceLoader) Option {
return func(o *options) { return func(o *options) {
o.serviceLoader = loader o.serviceLoader = loader

18
pkg/cache/cache.go vendored
View File

@ -13,23 +13,29 @@ var (
type Cache interface { type Cache interface {
// Get gets a cached value by key. // Get gets a cached value by key.
Get(ctx context.Context, key string) (any, time.Time, error) Load(ctx context.Context, key string, val any) error
// Get gets a cached value by key.
Exists(ctx context.Context, key string) (bool, error)
// Put stores a key-value pair into cache. // Put stores a key-value pair into cache.
Put(ctx context.Context, key string, val any, d time.Duration) error Store(ctx context.Context, key string, val any, d time.Duration) error
// Delete removes a key from cache. // Delete removes a key from cache.
Delete(ctx context.Context, key string) error Delete(ctx context.Context, key string) error
// String returns the name of the implementation. // String returns the name of the implementation.
String() string String() string
} }
func Default() Cache {
return std
}
// Get gets a cached value by key. // Get gets a cached value by key.
func Get(ctx context.Context, key string) (any, time.Time, error) { func Load(ctx context.Context, key string, val any) error {
return std.Get(ctx, key) return std.Load(ctx, key, val)
} }
// Put stores a key-value pair into cache. // Put stores a key-value pair into cache.
func Put(ctx context.Context, key string, val any, d time.Duration) error { func Store(ctx context.Context, key string, val any, d time.Duration) error {
return std.Put(ctx, key, val, d) return std.Store(ctx, key, val, d)
} }
// String returns the name of the implementation. // String returns the name of the implementation.

View File

@ -2,10 +2,18 @@ package memory
import ( import (
"context" "context"
"fmt"
"reflect"
"sync" "sync"
"time" "time"
"git.nobla.cn/golang/aeus/pkg/errors" "errors"
)
var (
ErrWongType = errors.New("val must be a pointer")
ErrNotExists = errors.New("not exists")
ErrAaddressable = errors.New("cannot set value: val is not addressable")
) )
type memCache struct { type memCache struct {
@ -15,22 +23,47 @@ type memCache struct {
sync.RWMutex sync.RWMutex
} }
func (c *memCache) Get(ctx context.Context, key string) (interface{}, time.Time, error) { func (c *memCache) Load(ctx context.Context, key string, val any) error {
c.RWMutex.RLock() c.RWMutex.RLock()
defer c.RWMutex.RUnlock() defer c.RWMutex.RUnlock()
item, found := c.items[key] item, found := c.items[key]
if !found { if !found {
return nil, time.Time{}, errors.ErrNotFound return ErrNotExists
} }
if item.Expired() { if item.Expired() {
return nil, time.Time{}, errors.ErrExpired return ErrNotExists
} }
refValue := reflect.ValueOf(val)
return item.Value, time.Unix(0, item.Expiration), nil if refValue.Type().Kind() != reflect.Ptr {
return ErrWongType
}
refElem := refValue.Elem()
if !refElem.CanSet() {
return ErrAaddressable
}
targetValue := reflect.Indirect(reflect.ValueOf(item.Value))
if targetValue.Type() != refElem.Type() {
return fmt.Errorf("type mismatch: expected %v, got %v", refElem.Type(), targetValue.Type())
}
refElem.Set(targetValue)
return nil
} }
func (c *memCache) Put(ctx context.Context, key string, val interface{}, d time.Duration) error { func (c *memCache) Exists(ctx context.Context, key string) (bool, error) {
c.RWMutex.RLock()
defer c.RWMutex.RUnlock()
item, found := c.items[key]
if !found {
return false, nil
}
if item.Expired() {
return false, nil
}
return true, nil
}
func (c *memCache) Store(ctx context.Context, key string, val any, d time.Duration) error {
var e int64 var e int64
if d == DefaultExpiration { if d == DefaultExpiration {
d = c.opts.Expiration d = c.opts.Expiration
@ -56,9 +89,8 @@ func (c *memCache) Delete(ctx context.Context, key string) error {
_, found := c.items[key] _, found := c.items[key]
if !found { if !found {
return errors.ErrNotFound return ErrNotExists
} }
delete(c.items, key) delete(c.items, key)
return nil return nil
} }
@ -66,3 +98,18 @@ func (c *memCache) Delete(ctx context.Context, key string) error {
func (m *memCache) String() string { func (m *memCache) String() string {
return "memory" return "memory"
} }
// NewCache returns a new cache.
func NewCache(opts ...Option) *memCache {
options := NewOptions(opts...)
items := make(map[string]Item)
if len(options.Items) > 0 {
items = options.Items
}
return &memCache{
opts: options,
items: items,
}
}

View File

@ -4,7 +4,7 @@ import "time"
// Item represents an item stored in the cache. // Item represents an item stored in the cache.
type Item struct { type Item struct {
Value interface{} Value any
Expiration int64 Expiration int64
} }
@ -16,18 +16,3 @@ func (i *Item) Expired() bool {
return time.Now().UnixNano() > i.Expiration return time.Now().UnixNano() > i.Expiration
} }
// NewCache returns a new cache.
func NewCache(opts ...Option) *memCache {
options := NewOptions(opts...)
items := make(map[string]Item)
if len(options.Items) > 0 {
items = options.Items
}
return &memCache{
opts: options,
items: items,
}
}

70
pkg/cache/redis/cache.go vendored 100644
View File

@ -0,0 +1,70 @@
package redis
import (
"context"
"encoding/json"
"time"
"git.nobla.cn/golang/aeus"
)
type redisCache struct {
opts *options
}
func (c *redisCache) buildKey(key string) string {
return c.opts.prefix + key
}
func (c *redisCache) Load(ctx context.Context, key string, val any) error {
var (
err error
buf []byte
)
if buf, err = c.opts.client.Get(ctx, c.buildKey(key)).Bytes(); err == nil {
err = json.Unmarshal(buf, val)
}
return err
}
func (c *redisCache) Exists(ctx context.Context, key string) (ok bool, err error) {
var n int64
n, err = c.opts.client.Exists(ctx, c.buildKey(key)).Result()
if n > 0 {
ok = true
}
return
}
// Put stores a key-value pair into cache.
func (c *redisCache) Store(ctx context.Context, key string, val any, d time.Duration) error {
var (
err error
buf []byte
)
if buf, err = json.Marshal(val); err == nil {
err = c.opts.client.Set(ctx, c.buildKey(key), buf, d).Err()
}
return err
}
// Delete removes a key from cache.
func (c *redisCache) Delete(ctx context.Context, key string) error {
return c.opts.client.Del(ctx, c.buildKey(key)).Err()
}
// String returns the name of the implementation.
func (c *redisCache) String() string {
return "redis"
}
func NewCache(opts ...Option) *redisCache {
cache := &redisCache{
opts: newOptions(opts...),
}
app := aeus.FromContext(cache.opts.context)
if app != nil {
cache.opts.prefix = app.Name() + ":" + cache.opts.prefix
}
return cache
}

46
pkg/cache/redis/types.go vendored 100644
View File

@ -0,0 +1,46 @@
package redis
import (
"context"
"github.com/redis/go-redis/v9"
)
type (
options struct {
context context.Context
client *redis.Client
prefix string
}
Option func(*options)
)
func WithClient(client *redis.Client) Option {
return func(o *options) {
o.client = client
}
}
func WithContext(ctx context.Context) Option {
return func(o *options) {
o.context = ctx
}
}
func WithPrefix(prefix string) Option {
return func(o *options) {
o.prefix = prefix
}
}
// NewOptions returns a new options struct.
func newOptions(opts ...Option) *options {
options := &options{
prefix: "cache:",
}
for _, o := range opts {
o(options)
}
return options
}

View File

@ -6,6 +6,7 @@ const (
Invalid = 1001 //payload invalid Invalid = 1001 //payload invalid
Exists = 1002 //already exists Exists = 1002 //already exists
Unavailable = 1003 //service unavailable Unavailable = 1003 //service unavailable
Incompatible = 1004 //type incompatible
Timeout = 2001 //timeout Timeout = 2001 //timeout
Expired = 2002 //expired Expired = 2002 //expired
TokenExpired = 4002 //token expired TokenExpired = 4002 //token expired
@ -29,4 +30,5 @@ var (
ErrUnavailable = New(Unavailable, "service unavailable") ErrUnavailable = New(Unavailable, "service unavailable")
ErrNetworkUnreachable = New(NetworkUnreachable, "network unreachable") ErrNetworkUnreachable = New(NetworkUnreachable, "network unreachable")
ErrConnectionRefused = New(ConnectionRefused, "connection refused") ErrConnectionRefused = New(ConnectionRefused, "connection refused")
ErrIncompatible = New(Incompatible, "incompatible")
) )

View File

@ -14,6 +14,14 @@ func (e *Error) Error() string {
return fmt.Sprintf("code: %d, message: %s", e.Code, e.Message) return fmt.Sprintf("code: %d, message: %s", e.Code, e.Message)
} }
func (e *Error) GetCode() int {
return e.Code
}
func (e *Error) GetMessage() string {
return e.Message
}
func Warp(code int, err error) error { func Warp(code int, err error) error {
return &Error{ return &Error{
Code: code, Code: code,

View File

@ -0,0 +1,27 @@
package httpclient
import (
"encoding/base64"
"fmt"
)
type Authorization interface {
Token() string
}
type BasicAuth struct {
Username string
Password string
}
type BearerAuth struct {
AccessToken string
}
func (auth *BasicAuth) Token() string {
return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(auth.Username+":"+auth.Password)))
}
func (auth *BearerAuth) Token() string {
return fmt.Sprintf("Bearer %s", auth.AccessToken)
}

View File

@ -0,0 +1,174 @@
package httpclient
import (
"crypto/tls"
"io"
"net"
"net/http"
"net/http/cookiejar"
"strings"
"time"
)
type (
BeforeRequest func(req *http.Request) (err error)
AfterRequest func(req *http.Request, res *http.Response) (err error)
Client struct {
baseUrl string
Authorization Authorization
client *http.Client
cookieJar *cookiejar.Jar
interceptorRequest []BeforeRequest
interceptorResponse []AfterRequest
}
)
var (
DefaultClient = &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
Proxy: http.ProxyFromEnvironment,
ForceAttemptHTTP2: true,
MaxIdleConns: 64,
MaxIdleConnsPerHost: 8,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
Timeout: time.Second * 30,
}
)
func (client *Client) stashUri(urlPath string) string {
var (
pos int
)
if len(urlPath) == 0 {
return client.baseUrl
}
if pos = strings.Index(urlPath, "//"); pos == -1 {
if client.baseUrl != "" {
if urlPath[0] != '/' {
urlPath = "/" + urlPath
}
return client.baseUrl + urlPath
}
}
return urlPath
}
func (client *Client) BeforeRequest(cb BeforeRequest) *Client {
client.interceptorRequest = append(client.interceptorRequest, cb)
return client
}
func (client *Client) AfterRequest(cb AfterRequest) *Client {
client.interceptorResponse = append(client.interceptorResponse, cb)
return client
}
func (client *Client) SetBaseUrl(s string) *Client {
client.baseUrl = strings.TrimSuffix(s, "/")
return client
}
func (client *Client) SetCookieJar(cookieJar *cookiejar.Jar) *Client {
client.client.Jar = cookieJar
return client
}
func (client *Client) SetClient(httpClient *http.Client) *Client {
client.client = httpClient
if client.cookieJar != nil {
client.client.Jar = client.cookieJar
}
return client
}
func (client *Client) SetTransport(transport http.RoundTripper) *Client {
client.client.Transport = transport
return client
}
func (client *Client) Get(urlPath string) *Request {
return newRequest(http.MethodGet, client.stashUri(urlPath), client)
}
func (client *Client) Put(urlPath string) *Request {
return newRequest(http.MethodPut, client.stashUri(urlPath), client)
}
func (client *Client) Post(urlPath string) *Request {
return newRequest(http.MethodPost, client.stashUri(urlPath), client)
}
func (client *Client) Delete(urlPath string) *Request {
return newRequest(http.MethodDelete, client.stashUri(urlPath), client)
}
func (client *Client) execute(r *Request) (res *http.Response, err error) {
var (
n int
reader io.Reader
)
if r.contentType == "" && r.body != nil {
r.contentType = r.detectContentType(r.body)
}
if r.body != nil {
if reader, err = r.readRequestBody(r.contentType, r.body); err != nil {
return
}
}
if r.rawRequest, err = http.NewRequest(r.method, r.uri, reader); err != nil {
return
}
for k, vs := range r.header {
for _, v := range vs {
r.rawRequest.Header.Add(k, v)
}
}
if r.contentType != "" {
r.rawRequest.Header.Set("Content-Type", r.contentType)
}
if client.Authorization != nil {
r.rawRequest.Header.Set("Authorization", client.Authorization.Token())
}
if r.context != nil {
r.rawRequest = r.rawRequest.WithContext(r.context)
}
n = len(client.interceptorRequest)
for i := n - 1; i >= 0; i-- {
if err = client.interceptorRequest[i](r.rawRequest); err != nil {
return
}
}
if r.rawResponse, err = client.client.Do(r.rawRequest); err != nil {
return nil, err
}
n = len(client.interceptorResponse)
for i := n - 1; i >= 0; i-- {
if err = client.interceptorResponse[i](r.rawRequest, r.rawResponse); err != nil {
_ = r.rawResponse.Body.Close()
return
}
}
return r.rawResponse, err
}
func New() *Client {
client := &Client{
client: DefaultClient,
interceptorRequest: make([]BeforeRequest, 0, 10),
interceptorResponse: make([]AfterRequest, 0, 10),
}
client.cookieJar, _ = cookiejar.New(nil)
client.client.Jar = client.cookieJar
return client
}

View File

@ -0,0 +1,28 @@
package httpclient
import (
"bytes"
"encoding/json"
"io"
"strings"
)
func encodeBody(data any) (r io.Reader, contentType string, err error) {
var (
buf []byte
)
switch v := data.(type) {
case string:
r = strings.NewReader(v)
contentType = "x-www-form-urlencoded"
case []byte:
r = bytes.NewReader(v)
contentType = "x-www-form-urlencoded"
default:
if buf, err = json.Marshal(v); err == nil {
r = bytes.NewReader(buf)
contentType = "application/json"
}
}
return
}

View File

@ -0,0 +1,162 @@
package httpclient
import (
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
)
func doHttpRequest(req *http.Request, opts *options) (res *http.Response, err error) {
if opts.human {
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.54")
}
if req.Header.Get("Referer") == "" {
req.Header.Set("Referer", req.URL.String())
}
}
return opts.client.Do(req)
}
// Get performs a GET request to the specified URL with optional parameters and headers.
func Get(ctx context.Context, urlString string, cbs ...Option) (res *http.Response, err error) {
var (
uri *url.URL
req *http.Request
)
opts := newOptions()
for _, cb := range cbs {
cb(opts)
}
if uri, err = url.Parse(urlString); err != nil {
return
}
if opts.params != nil {
qs := uri.Query()
for k, v := range opts.params {
qs.Set(k, v)
}
uri.RawQuery = qs.Encode()
}
if req, err = http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil); err != nil {
return
}
if opts.header != nil {
for k, v := range opts.header {
req.Header.Set(k, v)
}
}
return doHttpRequest(req, opts)
}
// Post performs a POST request to the specified URL with optional parameters, headers, and data.
func Post(ctx context.Context, urlString string, cbs ...Option) (res *http.Response, err error) {
var (
uri *url.URL
req *http.Request
contentType string
reader io.Reader
)
opts := newOptions()
for _, cb := range cbs {
cb(opts)
}
if uri, err = url.Parse(urlString); err != nil {
return
}
if opts.params != nil {
qs := uri.Query()
for k, v := range opts.params {
qs.Set(k, v)
}
uri.RawQuery = qs.Encode()
}
if opts.body != nil {
if reader, contentType, err = encodeBody(opts.body); err != nil {
return
}
}
if req, err = http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), reader); err != nil {
return
}
if opts.header != nil {
for k, v := range opts.header {
req.Header.Set(k, v)
}
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
return doHttpRequest(req, opts)
}
// Do performs a request to the specified URL with optional parameters, headers, and data.
func Do(ctx context.Context, urlString string, result any, cbs ...Option) (err error) {
var (
contentType string
reader io.Reader
uri *url.URL
res *http.Response
req *http.Request
)
opts := newOptions()
for _, cb := range cbs {
cb(opts)
}
if uri, err = url.Parse(urlString); err != nil {
return
}
if opts.params != nil {
qs := uri.Query()
for k, v := range opts.params {
qs.Set(k, v)
}
uri.RawQuery = qs.Encode()
}
if opts.body != nil {
if reader, contentType, err = encodeBody(opts.body); err != nil {
return
}
}
if req, err = http.NewRequestWithContext(ctx, opts.method, uri.String(), reader); err != nil {
return
}
if opts.header != nil {
for k, v := range opts.header {
req.Header.Set(k, v)
}
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
if res, err = doHttpRequest(req, opts); err != nil {
return
}
defer func() {
_ = res.Body.Close()
}()
if res.StatusCode != http.StatusOK {
err = fmt.Errorf("unexpected status %s(%d)", res.Status, res.StatusCode)
return
}
//don't care response
if result == nil {
return nil
}
contentType = strings.ToLower(res.Header.Get("Content-Type"))
extName := path.Ext(req.URL.String())
if strings.Contains(contentType, JSON) || extName == ".json" {
err = json.NewDecoder(res.Body).Decode(result)
} else if strings.Contains(contentType, XML) || extName == ".xml" {
err = xml.NewDecoder(res.Body).Decode(result)
} else {
err = fmt.Errorf("unsupported content type: %s", contentType)
}
return
}

View File

@ -0,0 +1,235 @@
package httpclient
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"reflect"
"regexp"
"strings"
)
const (
JSON = "application/json"
XML = "application/xml"
plainTextType = "text/plain; charset=utf-8"
jsonContentType = "application/json"
formContentType = "application/x-www-form-urlencoded"
)
var (
jsonCheck = regexp.MustCompile(`(?i:(application|text)/(json|.*\+json|json\-.*)(;|$))`)
xmlCheck = regexp.MustCompile(`(?i:(application|text)/(xml|.*\+xml)(;|$))`)
)
type Request struct {
context context.Context
method string
uri string
url *url.URL
body any
query url.Values
formData url.Values
header http.Header
contentType string
authorization Authorization
client *Client
rawRequest *http.Request
rawResponse *http.Response
}
func (r *Request) detectContentType(body any) string {
contentType := plainTextType
kind := reflect.Indirect(reflect.ValueOf(body)).Type().Kind()
switch kind {
case reflect.Struct, reflect.Map:
contentType = jsonContentType
case reflect.String:
contentType = plainTextType
default:
if b, ok := body.([]byte); ok {
contentType = http.DetectContentType(b)
} else if kind == reflect.Slice {
contentType = jsonContentType
}
}
return contentType
}
func (r *Request) readRequestBody(contentType string, body any) (reader io.Reader, err error) {
var (
ok bool
s string
buf []byte
)
kind := reflect.Indirect(reflect.ValueOf(body)).Type().Kind()
if reader, ok = r.body.(io.Reader); ok {
return reader, nil
}
if buf, ok = r.body.([]byte); ok {
goto __end
}
if s, ok = r.body.(string); ok {
buf = []byte(s)
goto __end
}
if jsonCheck.MatchString(contentType) && (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) {
buf, err = json.Marshal(r.body)
goto __end
}
if xmlCheck.MatchString(contentType) && (kind == reflect.Struct) {
buf, err = xml.Marshal(r.body)
goto __end
}
err = fmt.Errorf("unmarshal content type %s", contentType)
__end:
if err == nil {
if len(buf) > 0 {
return bytes.NewReader(buf), nil
}
}
return
}
func (r *Request) SetContext(ctx context.Context) *Request {
r.context = ctx
return r
}
func (r *Request) AddQuery(k, v string) *Request {
r.query.Add(k, v)
return r
}
func (r *Request) SetQuery(vs map[string]string) *Request {
for k, v := range vs {
r.query.Set(k, v)
}
return r
}
func (r *Request) AddFormData(k, v string) *Request {
r.contentType = formContentType
r.formData.Add(k, v)
return r
}
func (r *Request) SetFormData(vs map[string]string) *Request {
r.contentType = formContentType
for k, v := range vs {
r.formData.Set(k, v)
}
return r
}
func (r *Request) SetBody(v any) *Request {
r.body = v
return r
}
func (r *Request) SetContentType(v string) *Request {
r.contentType = v
return r
}
func (r *Request) AddHeader(k, v string) *Request {
r.header.Add(k, v)
return r
}
func (r *Request) SetHeader(h http.Header) *Request {
r.header = h
return r
}
func (r *Request) Do() (res *http.Response, err error) {
var s string
s = r.formData.Encode()
if len(s) > 0 {
r.body = s
}
r.url.RawQuery = r.query.Encode()
r.uri = r.url.String()
return r.client.execute(r)
}
func (r *Request) Response(v any) (err error) {
var (
res *http.Response
buf []byte
contentType string
)
if res, err = r.Do(); err != nil {
return
}
defer func() {
_ = res.Body.Close()
}()
if res.StatusCode/100 != 2 {
if buf, err = io.ReadAll(res.Body); err == nil && len(buf) > 0 {
err = fmt.Errorf("http response %s(%d): %s", res.Status, res.StatusCode, string(buf))
} else {
err = fmt.Errorf("http response %d: %s", res.StatusCode, res.Status)
}
return
}
contentType = strings.ToLower(res.Header.Get("Content-Type"))
extName := path.Ext(r.rawRequest.URL.String())
if strings.Contains(contentType, JSON) || extName == ".json" {
err = json.NewDecoder(res.Body).Decode(v)
} else if strings.Contains(contentType, XML) || extName == ".xml" {
err = xml.NewDecoder(res.Body).Decode(v)
} else {
err = fmt.Errorf("unsupported content type: %s", contentType)
}
return
}
func (r *Request) Download(s string) (err error) {
var (
fp *os.File
res *http.Response
)
if res, err = r.Do(); err != nil {
return
}
defer func() {
_ = res.Body.Close()
}()
if fp, err = os.OpenFile(s, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
return
}
defer func() {
_ = fp.Close()
}()
_, err = io.Copy(fp, res.Body)
return
}
func newRequest(method string, uri string, client *Client) *Request {
var (
err error
)
r := &Request{
context: context.Background(),
method: method,
uri: uri,
header: make(http.Header),
formData: make(url.Values),
client: client,
}
if r.url, err = url.Parse(uri); err == nil {
r.query = r.url.Query()
} else {
r.query = make(url.Values)
}
return r
}

View File

@ -0,0 +1,71 @@
package httpclient
import (
"maps"
"net/http"
)
type (
options struct {
url string
method string
header map[string]string
params map[string]string
body any
human bool
client *http.Client
}
Option func(o *options)
)
func WithUrl(s string) Option {
return func(o *options) {
o.url = s
}
}
func WithMethod(s string) Option {
return func(o *options) {
o.method = s
}
}
func WithHuman() Option {
return func(o *options) {
o.human = true
}
}
func WithClient(c *http.Client) Option {
return func(o *options) {
o.client = c
}
}
func WithHeader(h map[string]string) Option {
return func(o *options) {
if o.header == nil {
o.header = make(map[string]string)
}
maps.Copy(o.header, h)
}
}
func WithParams(h map[string]string) Option {
return func(o *options) {
o.params = h
}
}
func WithBody(v any) Option {
return func(o *options) {
o.body = v
}
}
func newOptions() *options {
return &options{
client: DefaultClient,
method: http.MethodGet,
}
}

View File

@ -31,6 +31,16 @@ type RestFieldOptions struct {
Format string `protobuf:"bytes,5,opt,name=format,proto3" json:"format,omitempty"` Format string `protobuf:"bytes,5,opt,name=format,proto3" json:"format,omitempty"`
Props string `protobuf:"bytes,6,opt,name=props,proto3" json:"props,omitempty"` Props string `protobuf:"bytes,6,opt,name=props,proto3" json:"props,omitempty"`
Rule string `protobuf:"bytes,7,opt,name=rule,proto3" json:"rule,omitempty"` Rule string `protobuf:"bytes,7,opt,name=rule,proto3" json:"rule,omitempty"`
Live string `protobuf:"bytes,8,opt,name=live,proto3" json:"live,omitempty"`
Dropdown string `protobuf:"bytes,9,opt,name=dropdown,proto3" json:"dropdown,omitempty"`
Enum string `protobuf:"bytes,10,opt,name=enum,proto3" json:"enum,omitempty"`
Match string `protobuf:"bytes,11,opt,name=match,proto3" json:"match,omitempty"`
Invisible string `protobuf:"bytes,12,opt,name=invisible,proto3" json:"invisible,omitempty"`
Tooltip string `protobuf:"bytes,13,opt,name=tooltip,proto3" json:"tooltip,omitempty"`
Uploaduri string `protobuf:"bytes,14,opt,name=uploaduri,proto3" json:"uploaduri,omitempty"`
Description string `protobuf:"bytes,15,opt,name=description,proto3" json:"description,omitempty"`
Readonly string `protobuf:"bytes,16,opt,name=readonly,proto3" json:"readonly,omitempty"`
Endofnow string `protobuf:"bytes,17,opt,name=endofnow,proto3" json:"endofnow,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -114,6 +124,76 @@ func (x *RestFieldOptions) GetRule() string {
return "" return ""
} }
func (x *RestFieldOptions) GetLive() string {
if x != nil {
return x.Live
}
return ""
}
func (x *RestFieldOptions) GetDropdown() string {
if x != nil {
return x.Dropdown
}
return ""
}
func (x *RestFieldOptions) GetEnum() string {
if x != nil {
return x.Enum
}
return ""
}
func (x *RestFieldOptions) GetMatch() string {
if x != nil {
return x.Match
}
return ""
}
func (x *RestFieldOptions) GetInvisible() string {
if x != nil {
return x.Invisible
}
return ""
}
func (x *RestFieldOptions) GetTooltip() string {
if x != nil {
return x.Tooltip
}
return ""
}
func (x *RestFieldOptions) GetUploaduri() string {
if x != nil {
return x.Uploaduri
}
return ""
}
func (x *RestFieldOptions) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *RestFieldOptions) GetReadonly() string {
if x != nil {
return x.Readonly
}
return ""
}
func (x *RestFieldOptions) GetEndofnow() string {
if x != nil {
return x.Endofnow
}
return ""
}
type RestMessageOptions struct { type RestMessageOptions struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Table string `protobuf:"bytes,1,opt,name=table,proto3" json:"table,omitempty"` Table string `protobuf:"bytes,1,opt,name=table,proto3" json:"table,omitempty"`
@ -198,7 +278,7 @@ var File_rest_proto protoreflect.FileDescriptor
const file_rest_proto_rawDesc = "" + const file_rest_proto_rawDesc = "" +
"\n" + "\n" +
"\n" + "\n" +
"rest.proto\x12\x04aeus\x1a google/protobuf/descriptor.proto\"\xbc\x01\n" + "rest.proto\x12\x04aeus\x1a google/protobuf/descriptor.proto\"\xc6\x03\n" +
"\x10RestFieldOptions\x12\x12\n" + "\x10RestFieldOptions\x12\x12\n" +
"\x04gorm\x18\x01 \x01(\tR\x04gorm\x12\x18\n" + "\x04gorm\x18\x01 \x01(\tR\x04gorm\x12\x18\n" +
"\acomment\x18\x02 \x01(\tR\acomment\x12\x1c\n" + "\acomment\x18\x02 \x01(\tR\acomment\x12\x1c\n" +
@ -206,7 +286,18 @@ const file_rest_proto_rawDesc = "" +
"\bposition\x18\x04 \x01(\tR\bposition\x12\x16\n" + "\bposition\x18\x04 \x01(\tR\bposition\x12\x16\n" +
"\x06format\x18\x05 \x01(\tR\x06format\x12\x14\n" + "\x06format\x18\x05 \x01(\tR\x06format\x12\x14\n" +
"\x05props\x18\x06 \x01(\tR\x05props\x12\x12\n" + "\x05props\x18\x06 \x01(\tR\x05props\x12\x12\n" +
"\x04rule\x18\a \x01(\tR\x04rule\"*\n" + "\x04rule\x18\a \x01(\tR\x04rule\x12\x12\n" +
"\x04live\x18\b \x01(\tR\x04live\x12\x1a\n" +
"\bdropdown\x18\t \x01(\tR\bdropdown\x12\x12\n" +
"\x04enum\x18\n" +
" \x01(\tR\x04enum\x12\x14\n" +
"\x05match\x18\v \x01(\tR\x05match\x12\x1c\n" +
"\tinvisible\x18\f \x01(\tR\tinvisible\x12\x18\n" +
"\atooltip\x18\r \x01(\tR\atooltip\x12\x1c\n" +
"\tuploaduri\x18\x0e \x01(\tR\tuploaduri\x12 \n" +
"\vdescription\x18\x0f \x01(\tR\vdescription\x12\x1a\n" +
"\breadonly\x18\x10 \x01(\tR\breadonly\x12\x1a\n" +
"\bendofnow\x18\x11 \x01(\tR\bendofnow\"*\n" +
"\x12RestMessageOptions\x12\x14\n" + "\x12RestMessageOptions\x12\x14\n" +
"\x05table\x18\x01 \x01(\tR\x05table:M\n" + "\x05table\x18\x01 \x01(\tR\x05table:M\n" +
"\x05field\x12\x1d.google.protobuf.FieldOptions\x18\x96\x97\x03 \x01(\v2\x16.aeus.RestFieldOptionsR\x05field:O\n" + "\x05field\x12\x1d.google.protobuf.FieldOptions\x18\x96\x97\x03 \x01(\v2\x16.aeus.RestFieldOptionsR\x05field:O\n" +

View File

@ -21,6 +21,16 @@ message RestFieldOptions {
string format = 5; string format = 5;
string props = 6; string props = 6;
string rule= 7; string rule= 7;
string live = 8;
string dropdown = 9;
string enum = 10;
string match = 11;
string invisible = 12;
string tooltip = 13;
string uploaduri = 14;
string description = 15;
string readonly = 16;
string endofnow = 17;
} }
extend google.protobuf.MessageOptions { extend google.protobuf.MessageOptions {

View File

@ -1,122 +0,0 @@
package generator
import (
"bytes"
"embed"
"html/template"
"io"
"io/fs"
"os"
"path"
"time"
"git.nobla.cn/golang/aeus/tools/gen/internal/types"
)
var (
fileMap = map[string]string{
"cmd/main.go": MainTemp,
"internal/scope/scope.go": ScopeTemp,
"internal/service/service.go": ServiceLoaderTemp,
"api/v1/pb/greeter.proto": GreeterTemp,
"version/version.go": VersionTemp,
"Makefile": MakefileTemp,
".gitignore": GitIgnoreTemp,
"README.md": ReadmeTemp,
"go.mod": GoModTemp,
"webhook.yaml": WebhookTemp,
"deploy/docker/deployment.yaml": DeploymentTemp,
}
)
var (
//go:embed third_party
protoDir embed.FS
)
type (
TemplateData struct {
ShortName string
PackageName string
Datetime string
Version string
ImageRegistry string
}
)
func writeFile(file string, buf []byte) (err error) {
dirname := path.Dir(file)
if _, err = os.Stat(dirname); err != nil {
if err = os.MkdirAll(dirname, 0755); err != nil {
return
}
}
err = os.WriteFile(file, buf, 0644)
return
}
func scanDir(s embed.FS, dirname string, callback func(file string) error) (err error) {
var (
entities []fs.DirEntry
)
if entities, err = s.ReadDir(dirname); err != nil {
return nil
}
for _, entity := range entities {
if entity.Name() == "." || entity.Name() == ".." {
continue
}
name := path.Join(dirname, entity.Name())
if entity.IsDir() {
scanDir(s, name, callback)
} else {
if err = callback(name); err != nil {
break
}
}
}
return
}
func Geerate(app *types.Applicetion) (err error) {
shortName := app.ShortName()
data := TemplateData{
ShortName: shortName,
PackageName: app.Package,
Version: app.Version,
ImageRegistry: "{{IMAGE_REGISTRY_URL}}",
Datetime: time.Now().Format(time.DateTime),
}
if data.Version == "" {
data.Version = "v0.0.1"
}
var t *template.Template
writer := bytes.NewBuffer(nil)
for name, tmpl := range fileMap {
if t, err = template.New(name).Parse(tmpl); err != nil {
return
}
if err = t.Execute(writer, data); err != nil {
return
}
if err = writeFile(path.Join(shortName, name), writer.Bytes()); err != nil {
return
}
writer.Reset()
}
if err = writeFile(shortName+".go", []byte("package "+shortName)); err != nil {
return
}
err = scanDir(protoDir, "third_party", func(filename string) error {
if fp, openerr := protoDir.Open(filename); openerr != nil {
return openerr
} else {
if buf, readerr := io.ReadAll(fp); readerr == nil {
writeFile(path.Join(shortName, filename), buf)
}
fp.Close()
}
return nil
})
return
}

View File

@ -1,271 +0,0 @@
package generator
var (
MainTemp = `
package main
import (
"fmt"
"os"
"flag"
"git.nobla.cn/golang/aeus"
"git.nobla.cn/golang/aeus/transport/cli"
"git.nobla.cn/golang/aeus/transport/grpc"
"git.nobla.cn/golang/aeus/transport/http"
"{{.PackageName}}/version"
"{{.PackageName}}/internal/scope"
"{{.PackageName}}/internal/service"
)
var (
versionFlag = flag.Bool("version", false, "Show version")
)
func main() {
var (
err error
)
flag.Parse()
if *versionFlag{
fmt.Println(version.Info())
os.Exit(0)
}
app := aeus.New(
aeus.WithName(version.ProductName),
aeus.WithVersion(version.Version),
aeus.WithServer(
http.New(),
grpc.New(),
cli.New(),
),
aeus.WithScope(scope.NewScope()),
aeus.WithServiceLoader(service.NewLoader()),
)
if err = app.Run(); err != nil {
fmt.Println("app run error:", err)
os.Exit(1)
}
}
`
ScopeTemp = `
package scope
import (
"context"
"git.nobla.cn/golang/aeus/transport/cli"
"git.nobla.cn/golang/aeus/transport/grpc"
"git.nobla.cn/golang/aeus/transport/http"
)
type ScopeContext struct {
ctx context.Context
Http *http.Server
Grpc *grpc.Server
Cli *cli.Server
}
func (s *ScopeContext) Init(ctx context.Context) (err error) {
s.ctx = ctx
return
}
func NewScope() *ScopeContext {
return &ScopeContext{}
}
`
ServiceLoaderTemp = `
package service
import (
"context"
"{{.PackageName}}/internal/scope"
"git.nobla.cn/golang/aeus/transport/cli"
"git.nobla.cn/golang/aeus/transport/grpc"
"git.nobla.cn/golang/aeus/transport/http"
)
type serviceLoader struct {
Sope *scope.ScopeContext
Http *http.Server
Grpc *grpc.Server
Cli *cli.Server
}
func (s *serviceLoader) Init(ctx context.Context) (err error) {
// bind services here
return
}
func (s *serviceLoader) Run(ctx context.Context) (err error) {
return
}
func NewLoader() *serviceLoader {
return &serviceLoader{}
}
`
MakefileTemp = `
GOHOSTOS:=$(shell go env GOHOSTOS)
GOPATH:=$(shell go env GOPATH)
VERSION=$(shell git describe --tags --always)
DATETIME:=$(shell date "+%Y-%m-%d %H:%M:%S")
PROTO_DIR="api/v1/pb"
PROTO_OUT_DIR="api/v1/pb"
PROTO_FILES=$(shell find api -name *.proto)
.PHONY: proto
proto:
protoc --proto_path=$(PROTO_DIR) \
--proto_path=./third_party \
--go_out=paths=source_relative:$(PROTO_OUT_DIR) \
--go-grpc_out=paths=source_relative:$(PROTO_OUT_DIR) \
--go-aeus_out=paths=source_relative:$(PROTO_OUT_DIR) \
--validate_out=paths=source_relative,lang=go:$(PROTO_OUT_DIR) \
$(PROTO_FILES)
.PHONY: proto-clean
proto-clean:
rm -rf $(PROTO_OUT_DIR)/*.pb.go
rm -rf $(PROTO_OUT_DIR)/*.pb.validate.go
.PHONY: docker
docker:
docker build . -t $(IMAGE_REGISTRY_URL)
.PHONY: deploy
deploy:
dkctl apply -f deployment.yaml
.PHONY: build
build:
go mod tidy
go mod vendor
CGO_ENABLED=0 go build -a -installsuffix cgo -ldflags "-s -w -X '{{.PackageName}}/version.Version=$(VERSION)' -X '{{.PackageName}}/version.BuildDate=$(DATETIME)'" -o bin/{{.ShortName}} cmd/main.go
`
GreeterTemp = `
syntax = "proto3";
package greeter;
import "google/api/annotations.proto";
import "aeus/command.proto";
import "aeus/rest.proto";
import "validate/validate.proto";
option go_package = "{{.PackageName}}/api/v1/pb;pb";
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
get: "/helloworld/{name}"
};
option (aeus.command) = {
path: "/helloworld/:name",
description: "Greeter"
};
}
}
// The request message containing the user's name.
message HelloRequest {
option (aeus.rest) = {
table: "users"
};
int64 id = 1 [(aeus.field)={gorm:"primary_key"},(validate.rules).int64.gt = 999];
string name = 2;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
`
VersionTemp = `
package version
import "fmt"
var (
Version = "{{.Version}}"
BuildDate = "{{.Datetime}}"
ProductName = "{{.ShortName}}"
)
func Info() string {
return fmt.Sprintf("%s version: %s (built at %s)", ProductName, Version, BuildDate)
}
`
GitIgnoreTemp = `
.vscode
.idea
bin/
.svn/
.godeps
./build
.cover/
*.dat
vendor
*.o
*.a
*.so
# Folders
_obj
_test
`
ReadmeTemp = ``
GoModTemp = `
module {{.PackageName}}
go 1.23.0
`
WebhookTemp = `
name: {{.ShortName}}
steps:
- name: build
run: "make build"
- name: docker
run: "make docker"
- name: deploy
run: "make deploy"
replacements:
- src: deploy/docker/deployment.yaml
dst: deployment.yaml
`
DeploymentTemp = `
name: {{.ShortName}}
image: {{.ImageRegistry}}
command: ["{{.ShortName}}"]
network:
name: employ
ip: 10.5.10.2
env:
- name: TZ
value: "Asia/Shanghai"
- name: APP_NAME
value: "{{.ShortName}}"
volume:
- name: config
path: /etc/{{.ShortName}}/
hostPath: /apps/{{.ShortName}}/conf/
`
)

View File

@ -1,16 +0,0 @@
package types
import "strings"
type Applicetion struct {
Package string
Version string
}
func (app *Applicetion) ShortName() string {
pos := strings.LastIndex(app.Package, "/")
if pos > -1 {
return app.Package[pos+1:]
}
return app.Package
}

View File

@ -1,69 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"git.nobla.cn/golang/aeus/tools/gen/internal/generator"
"git.nobla.cn/golang/aeus/tools/gen/internal/types"
"github.com/spf13/cobra"
)
func waitingSignal(ctx context.Context, cancelFunc context.CancelFunc) {
ch := make(chan os.Signal, 1)
signals := []os.Signal{syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL}
signal.Notify(ch, signals...)
select {
case <-ctx.Done():
case <-ch:
cancelFunc()
close(ch)
}
}
func createCommand() *cobra.Command {
var (
version string
)
cmd := &cobra.Command{
Use: "new",
Short: "Create microservice application",
Long: "Create microservice application",
RunE: func(cmd *cobra.Command, args []string) (err error) {
if len(args) == 0 {
return fmt.Errorf("Please specify the package name")
}
if version, err = cmd.Flags().GetString("version"); err != nil {
return
}
return generator.Geerate(&types.Applicetion{
Package: args[0],
Version: version,
})
},
}
cmd.Flags().StringP("version", "v", "v0.0.1", "Application version")
return cmd
}
func main() {
var (
err error
)
ctx, cancelFunc := context.WithCancel(context.Background())
cmd := &cobra.Command{
Use: "aeus",
Short: "aeus is a tool for manager microservices",
Long: "aeus is a tool for manager microservice application",
SilenceErrors: true,
}
go waitingSignal(ctx, cancelFunc)
cmd.AddCommand(createCommand())
if err = cmd.ExecuteContext(ctx); err != nil {
fmt.Println(err.Error())
}
cancelFunc()
}

View File

@ -6,7 +6,9 @@ import (
"math" "math"
"net" "net"
"net/url" "net/url"
"os"
"runtime" "runtime"
"strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -243,6 +245,8 @@ func New(cbs ...Option) *Server {
uri: &url.URL{Scheme: "cli"}, uri: &url.URL{Scheme: "cli"},
router: newRouter(""), router: newRouter(""),
} }
port, _ := strconv.Atoi(os.Getenv("CLI_PORT"))
srv.opts.address = fmt.Sprintf(":%d", port)
for _, cb := range cbs { for _, cb := range cbs {
cb(srv.opts) cb(srv.opts)
} }

View File

@ -2,8 +2,11 @@ package grpc
import ( import (
"context" "context"
"fmt"
"net" "net"
"net/url" "net/url"
"os"
"strconv"
"git.nobla.cn/golang/aeus/metadata" "git.nobla.cn/golang/aeus/metadata"
"git.nobla.cn/golang/aeus/middleware" "git.nobla.cn/golang/aeus/middleware"
@ -136,7 +139,6 @@ func New(cbs ...Option) *Server {
opts: &options{ opts: &options{
network: "tcp", network: "tcp",
logger: logger.Default(), logger: logger.Default(),
address: ":0",
grpcOpts: make([]grpc.ServerOption, 0, 10), grpcOpts: make([]grpc.ServerOption, 0, 10),
}, },
uri: &url.URL{ uri: &url.URL{
@ -144,6 +146,8 @@ func New(cbs ...Option) *Server {
}, },
middlewares: make([]middleware.Middleware, 0, 10), middlewares: make([]middleware.Middleware, 0, 10),
} }
port, _ := strconv.Atoi(os.Getenv("GRPC_PORT"))
svr.opts.address = fmt.Sprintf(":%d", port)
for _, cb := range cbs { for _, cb := range cbs {
cb(svr.opts) cb(svr.opts)
} }

View File

@ -22,6 +22,10 @@ func (c *Context) Context() context.Context {
return c.ctx.Request.Context() return c.ctx.Request.Context()
} }
func (c *Context) Gin() *gin.Context {
return c.ctx
}
func (c *Context) Request() *http.Request { func (c *Context) Request() *http.Request {
return c.ctx.Request return c.ctx.Request
} }
@ -38,6 +42,14 @@ func (c *Context) Param(key string) string {
return c.ctx.Param(key) return c.ctx.Param(key)
} }
func (c *Context) Query(key string) string {
qs := c.ctx.Request.URL.Query()
if qs != nil {
return qs.Get(key)
}
return ""
}
func (c *Context) Bind(val any) (err error) { func (c *Context) Bind(val any) (err error) {
// if params exists, try bind params first // if params exists, try bind params first
if len(c.ctx.Params) > 0 { if len(c.ctx.Params) > 0 {

View File

@ -2,11 +2,18 @@ package http
import ( import (
"context" "context"
"fmt"
"io"
"net" "net"
"net/http" "net/http"
"net/http/pprof" "net/http/pprof"
"net/url" "net/url"
"os"
"path" "path"
"path/filepath"
"slices"
"strconv"
"strings"
"sync" "sync"
"time" "time"
@ -103,23 +110,99 @@ func (s *Server) Webroot(prefix string, fs http.FileSystem) {
s.fs.SetIndexFile("/index.html") s.fs.SetIndexFile("/index.html")
} }
func (s *Server) shouldCompress(req *http.Request) bool {
if !strings.Contains(req.Header.Get(headerAcceptEncoding), "gzip") ||
strings.Contains(req.Header.Get("Connection"), "Upgrade") {
return false
}
// Check if the request path is excluded from compression
extension := filepath.Ext(req.URL.Path)
if slices.Contains(assetsExtensions, extension) {
return true
}
return false
}
func (s *Server) staticHandle(ctx *gin.Context, fp http.File) {
uri := path.Clean(ctx.Request.URL.Path)
fi, err := fp.Stat()
if err != nil {
return
}
if !fi.IsDir() {
//https://github.com/gin-contrib/gzip
if s.shouldCompress(ctx.Request) && fi.Size() > 8192 {
gzWriter := newGzipWriter()
gzWriter.Reset(ctx.Writer)
ctx.Header(headerContentEncoding, "gzip")
ctx.Writer.Header().Add(headerVary, headerAcceptEncoding)
originalEtag := ctx.GetHeader("ETag")
if originalEtag != "" && !strings.HasPrefix(originalEtag, "W/") {
ctx.Header("ETag", "W/"+originalEtag)
}
ctx.Writer = &gzipWriter{ctx.Writer, gzWriter}
defer func() {
if ctx.Writer.Size() < 0 {
gzWriter.Reset(io.Discard)
}
gzWriter.Close()
if ctx.Writer.Size() > -1 {
ctx.Header("Content-Length", strconv.Itoa(ctx.Writer.Size()))
}
putGzipWriter(gzWriter)
}()
}
}
http.ServeContent(ctx.Writer, ctx.Request, path.Base(uri), s.fs.modtime, fp)
ctx.Abort()
return
}
func (s *Server) notFoundHandle(ctx *gin.Context) { func (s *Server) notFoundHandle(ctx *gin.Context) {
if s.fs != nil && ctx.Request.Method == http.MethodGet { if s.fs != nil && ctx.Request.Method == http.MethodGet {
uri := path.Clean(ctx.Request.URL.Path) uri := path.Clean(ctx.Request.URL.Path)
if fp, err := s.fs.Open(uri); err == nil { if fp, err := s.fs.Open(uri); err == nil {
http.ServeContent(ctx.Writer, ctx.Request, path.Base(uri), s.fs.modtime, fp) s.staticHandle(ctx, fp)
fp.Close() fp.Close()
ctx.Abort()
return
} }
} }
ctx.JSON(http.StatusNotFound, newResponse(errors.NotFound, "Not Found", nil)) ctx.JSON(http.StatusNotFound, newResponse(errors.NotFound, "Not Found", nil))
} }
func (s *Server) CORSInterceptor() gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method == "OPTIONS" {
c.Writer.Header().Add("Vary", "Origin")
c.Writer.Header().Add("Vary", "Access-Control-Request-Method")
c.Writer.Header().Add("Vary", "Access-Control-Request-Headers")
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE")
h := c.Request.Header.Get("Access-Control-Request-Headers")
if h != "" {
c.Writer.Header().Set("Access-Control-Allow-Headers", h)
}
c.AbortWithStatus(204)
return
} else {
c.Writer.Header().Add("Vary", "Origin")
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
h := c.Request.Header.Get("Access-Control-Request-Headers")
if h != "" {
c.Writer.Header().Set("Access-Control-Allow-Headers", h)
}
}
c.Next()
}
}
func (s *Server) requestInterceptor() gin.HandlerFunc { func (s *Server) requestInterceptor() gin.HandlerFunc {
return func(ginCtx *gin.Context) { return func(ginCtx *gin.Context) {
ctx := ginCtx.Request.Context() ctx := ginCtx.Request.Context()
next := func(ctx context.Context) error { next := func(ctx context.Context) error {
ginCtx.Request = ginCtx.Request.WithContext(ctx)
ginCtx.Next() ginCtx.Next()
if err := ginCtx.Errors.Last(); err != nil { if err := ginCtx.Errors.Last(); err != nil {
return err.Err return err.Err
@ -139,8 +222,8 @@ func (s *Server) requestInterceptor() gin.HandlerFunc {
} }
md.Set(metadata.RequestProtocolKey, Protocol) md.Set(metadata.RequestProtocolKey, Protocol)
md.Set(metadata.RequestPathKey, ginCtx.Request.URL.Path) md.Set(metadata.RequestPathKey, ginCtx.Request.URL.Path)
md.Set("method", ginCtx.Request.Method)
ctx = metadata.NewContext(ctx, md) ctx = metadata.NewContext(ctx, md)
ginCtx.Request = ginCtx.Request.WithContext(ctx)
if err := handler(ctx); err != nil { if err := handler(ctx); err != nil {
if se, ok := err.(*errors.Error); ok { if se, ok := err.(*errors.Error); ok {
ginCtx.AbortWithStatusJSON(http.StatusInternalServerError, newResponse(se.Code, se.Message, nil)) ginCtx.AbortWithStatusJSON(http.StatusInternalServerError, newResponse(se.Code, se.Message, nil))
@ -213,9 +296,10 @@ func New(cbs ...Option) *Server {
opts: &options{ opts: &options{
network: "tcp", network: "tcp",
logger: logger.Default(), logger: logger.Default(),
address: ":0",
}, },
} }
port, _ := strconv.Atoi(os.Getenv("HTTP_PORT"))
svr.opts.address = fmt.Sprintf(":%d", port)
for _, cb := range cbs { for _, cb := range cbs {
cb(svr.opts) cb(svr.opts)
} }
@ -223,6 +307,9 @@ func New(cbs ...Option) *Server {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
} }
svr.engine = gin.New(svr.opts.ginOptions...) svr.engine = gin.New(svr.opts.ginOptions...)
if svr.opts.enableCORS {
svr.engine.Use(svr.CORSInterceptor())
}
svr.engine.Use(svr.requestInterceptor()) svr.engine.Use(svr.requestInterceptor())
return svr return svr
} }

View File

@ -1,8 +1,14 @@
package http package http
import ( import (
"bufio"
"compress/gzip"
"context" "context"
"errors"
"io"
"net"
"net/http" "net/http"
"sync"
"git.nobla.cn/golang/aeus/pkg/logger" "git.nobla.cn/golang/aeus/pkg/logger"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -25,6 +31,7 @@ type (
logger logger.Logger logger logger.Logger
context context.Context context context.Context
ginOptions []gin.OptionFunc ginOptions []gin.OptionFunc
enableCORS bool
} }
HandleFunc func(ctx *Context) (err error) HandleFunc func(ctx *Context) (err error)
@ -40,12 +47,89 @@ type (
} }
) )
const (
headerAcceptEncoding = "Accept-Encoding"
headerContentEncoding = "Content-Encoding"
headerVary = "Vary"
)
var (
gzPool sync.Pool
assetsExtensions = []string{".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".otf"}
)
type gzipWriter struct {
gin.ResponseWriter
writer *gzip.Writer
}
func (g *gzipWriter) WriteString(s string) (int, error) {
g.Header().Del("Content-Length")
return g.writer.Write([]byte(s))
}
func (g *gzipWriter) Write(data []byte) (int, error) {
g.Header().Del("Content-Length")
return g.writer.Write(data)
}
func (g *gzipWriter) Flush() {
_ = g.writer.Flush()
g.ResponseWriter.Flush()
}
// Fix: https://github.com/mholt/caddy/issues/38
func (g *gzipWriter) WriteHeader(code int) {
g.Header().Del("Content-Length")
g.ResponseWriter.WriteHeader(code)
}
var _ http.Hijacker = (*gzipWriter)(nil)
// Hijack allows the caller to take over the connection from the HTTP server.
// After a call to Hijack, the HTTP server library will not do anything else with the connection.
// It becomes the caller's responsibility to manage and close the connection.
//
// It returns the underlying net.Conn, a buffered reader/writer for the connection, and an error
// if the ResponseWriter does not support the Hijacker interface.
func (g *gzipWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hijacker, ok := g.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface")
}
return hijacker.Hijack()
}
func newGzipWriter() (writer *gzip.Writer) {
v := gzPool.Get()
if v == nil {
writer, _ = gzip.NewWriterLevel(io.Discard, gzip.DefaultCompression)
} else {
if w, ok := v.(*gzip.Writer); ok {
return w
} else {
writer, _ = gzip.NewWriterLevel(io.Discard, gzip.DefaultCompression)
}
}
return
}
func putGzipWriter(writer *gzip.Writer) {
gzPool.Put(writer)
}
func WithNetwork(network string) Option { func WithNetwork(network string) Option {
return func(o *options) { return func(o *options) {
o.network = network o.network = network
} }
} }
func WithCORS() Option {
return func(o *options) {
o.enableCORS = true
}
}
func WithAddress(address string) Option { func WithAddress(address string) Option {
return func(o *options) { return func(o *options) {
o.address = address o.address = address