Compare commits

..

No commits in common. "main" and "v0.0.4" have entirely different histories.
main ... v0.0.4

34 changed files with 568 additions and 1656 deletions

1
.gitignore vendored
View File

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

View File

@ -4,99 +4,5 @@
# 环境变量
| 环境变量 | 描述 |
| --- | --- |
| 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 | 前端资源目录 |

28
app.go
View File

@ -6,7 +6,6 @@ import (
"os/signal" "os/signal"
"reflect" "reflect"
"runtime" "runtime"
"strconv"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time" "time"
@ -36,19 +35,12 @@ 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 {
if s.opts != nil { return s.opts.version
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
@ -109,14 +101,10 @@ func (s *Service) injectVars(v any) {
continue continue
} }
fieldType := refType.Field(i) fieldType := refType.Field(i)
if !(fieldType.Type.Kind() != reflect.Ptr || fieldType.Type.Kind() != reflect.Interface) { if fieldType.Type.Kind() != reflect.Ptr {
continue continue
} }
for _, rv := range s.refValues { for _, rv := range s.refValues {
if fieldType.Type.Kind() == reflect.Interface && rv.Type().Implements(fieldType.Type) {
refValue.Field(i).Set(rv)
break
}
if fieldType.Type == rv.Type() { if fieldType.Type == rv.Type() {
refValue.Field(i).Set(rv) refValue.Field(i).Set(rv)
break break
@ -126,11 +114,8 @@ 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(ctx, "starting") s.Logger().Info(s.ctx, "starting")
s.refValues = append(s.refValues, s.opts.injectVars...)
s.refValues = append(s.refValues, reflect.ValueOf(s.Logger()))
for _, ptr := range s.opts.servers { for _, ptr := range s.opts.servers {
s.injectVars(ptr)
s.refValues = append(s.refValues, reflect.ValueOf(ptr)) s.refValues = append(s.refValues, reflect.ValueOf(ptr))
} }
if s.opts.registry != nil { if s.opts.registry != nil {
@ -192,7 +177,7 @@ func (s *Service) preStart(ctx context.Context) (err error) {
o.Context = ctx o.Context = ctx
o.TTL = s.opts.registrarTimeout o.TTL = s.opts.registrarTimeout
}); err != nil { }); err != nil {
s.Logger().Warnf(ctx, "service register error: %v", err) s.Logger().Warn(ctx, "service register error: %v", err)
} }
} }
} }
@ -213,14 +198,14 @@ func (s *Service) preStop() (err error) {
}() }()
for _, srv := range s.opts.servers { for _, srv := range s.opts.servers {
if err = srv.Stop(ctx); err != nil { if err = srv.Stop(ctx); err != nil {
s.Logger().Warnf(ctx, "server stop error: %v", err) s.Logger().Warn(ctx, "server stop error: %v", err)
} }
} }
if s.opts.registry != nil { if s.opts.registry != nil {
if err = s.opts.registry.Deregister(s.service, func(o *registry.DeregisterOptions) { if err = s.opts.registry.Deregister(s.service, func(o *registry.DeregisterOptions) {
o.Context = ctx o.Context = ctx
}); err != nil { }); err != nil {
s.Logger().Warnf(ctx, "server deregister error: %v", err) s.Logger().Warn(ctx, "server deregister error: %v", err)
} }
} }
s.Logger().Info(ctx, "stopped") s.Logger().Info(ctx, "stopped")
@ -270,7 +255,6 @@ 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,7 +11,6 @@ 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
@ -25,12 +24,10 @@ 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,13 +1,7 @@
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=
@ -20,8 +14,6 @@ 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=
@ -94,8 +86,6 @@ 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,7 +2,6 @@ package auth
import ( import (
"context" "context"
"reflect"
"strings" "strings"
"git.nobla.cn/golang/aeus/metadata" "git.nobla.cn/golang/aeus/metadata"
@ -30,69 +29,48 @@ const (
type Option func(*options) type Option func(*options)
type Validate interface {
Validate(ctx context.Context, token string) error
}
// Parser is a jwt parser // Parser is a jwt parser
type options struct { type options struct {
allows []string allows []string
claims reflect.Type claims func() jwt.Claims
validate Validate }
// WithClaims with customer claim
// If you use it in Server, f needs to return a new jwt.Claims object each time to avoid concurrent write problems
// If you use it in Client, f only needs to return a single object to provide performance
func WithClaims(f func() jwt.Claims) Option {
return func(o *options) {
o.claims = f
}
} }
// WithAllow with allow path // WithAllow with allow path
func WithAllow(paths ...string) Option { func WithAllow(path string) Option {
return func(o *options) { return func(o *options) {
if o.allows == nil { if o.allows == nil {
o.allows = make([]string, 0, 16) o.allows = make([]string, 0, 16)
} }
for _, s := range paths { o.allows = append(o.allows, path)
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 _, pattern := range allows { for _, str := range allows {
n := len(pattern) n := len(str)
if pattern == uripath { if n == 0 {
return true continue
} }
if pattern == "*" { if n > 1 && str[n-1] == '*' {
return true if strings.HasPrefix(uripath, str[:n-1]) {
}
if n > 1 && pattern[n-1] == '*' {
if strings.HasPrefix(uripath, pattern[:n-1]) {
return true return true
} }
} }
if str == uripath {
return true
}
} }
return false return true
} }
// JWT auth middleware // JWT auth middleware
@ -104,6 +82,10 @@ func JWT(keyFunc jwt.Keyfunc, cbs ...Option) middleware.Middleware {
return func(next middleware.Handler) middleware.Handler { return func(next middleware.Handler) middleware.Handler {
return func(ctx context.Context) (err error) { return func(ctx context.Context) (err error) {
md := metadata.FromContext(ctx) md := metadata.FromContext(ctx)
authorizationValue, ok := md.Get(authorizationKey)
if !ok {
return errors.ErrAccessDenied
}
if len(opts.allows) > 0 { if len(opts.allows) > 0 {
requestPath, ok := md.Get(metadata.RequestPathKey) requestPath, ok := md.Get(metadata.RequestPathKey)
if ok { if ok {
@ -112,27 +94,17 @@ func JWT(keyFunc jwt.Keyfunc, cbs ...Option) middleware.Middleware {
} }
} }
} }
token, ok := md.Get(authorizationKey) if !strings.HasPrefix(authorizationValue, bearerWord) {
if !ok {
return errors.ErrAccessDenied return errors.ErrAccessDenied
} }
if opts.validate != nil {
if err = opts.validate.Validate(ctx, token); err != nil {
return err
}
}
token, _ = strings.CutPrefix(token, bearerWord)
var ( var (
ti *jwt.Token ti *jwt.Token
) )
token = strings.TrimSpace(token) authorizationToken := strings.TrimSpace(strings.TrimPrefix(authorizationValue, bearerWord))
if opts.claims != nil { if opts.claims != nil {
if claims, ok := reflect.New(opts.claims).Interface().(jwt.Claims); ok { ti, err = jwt.ParseWithClaims(authorizationToken, opts.claims(), keyFunc)
ti, err = jwt.ParseWithClaims(token, claims, keyFunc) } else {
} 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,8 +2,6 @@ package aeus
import ( import (
"context" "context"
"maps"
"reflect"
"time" "time"
"git.nobla.cn/golang/aeus/pkg/logger" "git.nobla.cn/golang/aeus/pkg/logger"
@ -20,12 +18,10 @@ 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
stopTimeout time.Duration stopTimeout time.Duration
injectVars []reflect.Value
} }
func WithName(name string) Option { func WithName(name string) Option {
@ -45,7 +41,9 @@ 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)
} }
maps.Copy(o.metadata, metadata) for k, v := range metadata {
o.metadata[k] = v
}
} }
} }
@ -73,20 +71,6 @@ func WithScope(scope Scope) Option {
} }
} }
func WithDebug(debug bool) Option {
return func(o *options) {
o.debug = debug
}
}
func WithInjectVars(vars ...any) Option {
return func(o *options) {
for _, v := range vars {
o.injectVars = append(o.injectVars, reflect.ValueOf(v))
}
}
}
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,29 +13,23 @@ var (
type Cache interface { type Cache interface {
// Get gets a cached value by key. // Get gets a cached value by key.
Load(ctx context.Context, key string, val any) error Get(ctx context.Context, key string) (any, time.Time, 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.
Store(ctx context.Context, key string, val any, d time.Duration) error Put(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 Load(ctx context.Context, key string, val any) error { func Get(ctx context.Context, key string) (any, time.Time, error) {
return std.Load(ctx, key, val) return std.Get(ctx, key)
} }
// Put stores a key-value pair into cache. // Put stores a key-value pair into cache.
func Store(ctx context.Context, key string, val any, d time.Duration) error { func Put(ctx context.Context, key string, val any, d time.Duration) error {
return std.Store(ctx, key, val, d) return std.Put(ctx, key, val, d)
} }
// String returns the name of the implementation. // String returns the name of the implementation.

View File

@ -2,18 +2,10 @@ package memory
import ( import (
"context" "context"
"fmt"
"reflect"
"sync" "sync"
"time" "time"
"errors" "git.nobla.cn/golang/aeus/pkg/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 {
@ -23,47 +15,22 @@ type memCache struct {
sync.RWMutex sync.RWMutex
} }
func (c *memCache) Load(ctx context.Context, key string, val any) error { func (c *memCache) Get(ctx context.Context, key string) (interface{}, time.Time, 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 ErrNotExists return nil, time.Time{}, errors.ErrNotFound
} }
if item.Expired() { if item.Expired() {
return ErrNotExists return nil, time.Time{}, errors.ErrExpired
} }
refValue := reflect.ValueOf(val)
if refValue.Type().Kind() != reflect.Ptr { return item.Value, time.Unix(0, item.Expiration), nil
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) Exists(ctx context.Context, key string) (bool, error) { func (c *memCache) Put(ctx context.Context, key string, val interface{}, d time.Duration) 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
@ -89,8 +56,9 @@ func (c *memCache) Delete(ctx context.Context, key string) error {
_, found := c.items[key] _, found := c.items[key]
if !found { if !found {
return ErrNotExists return errors.ErrNotFound
} }
delete(c.items, key) delete(c.items, key)
return nil return nil
} }
@ -98,18 +66,3 @@ 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 any Value interface{}
Expiration int64 Expiration int64
} }
@ -16,3 +16,18 @@ 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,
}
}

View File

@ -1,70 +0,0 @@
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
}

View File

@ -1,46 +0,0 @@
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,7 +6,6 @@ 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
@ -30,5 +29,4 @@ 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,14 +14,6 @@ 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

@ -1,27 +0,0 @@
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

@ -1,174 +0,0 @@
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

@ -1,28 +0,0 @@
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

@ -1,162 +0,0 @@
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

@ -1,235 +0,0 @@
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

@ -1,71 +0,0 @@
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

@ -11,34 +11,18 @@ type logger struct {
} }
func (l *logger) Debug(ctx context.Context, msg string, args ...any) { func (l *logger) Debug(ctx context.Context, msg string, args ...any) {
l.log.DebugContext(ctx, msg, args...)
}
func (l *logger) Debugf(ctx context.Context, msg string, args ...any) {
l.log.DebugContext(ctx, fmt.Sprintf(msg, args...)) l.log.DebugContext(ctx, fmt.Sprintf(msg, args...))
} }
func (l *logger) Info(ctx context.Context, msg string, args ...any) { func (l *logger) Info(ctx context.Context, msg string, args ...any) {
l.log.InfoContext(ctx, msg, args...)
}
func (l *logger) Infof(ctx context.Context, msg string, args ...any) {
l.log.InfoContext(ctx, fmt.Sprintf(msg, args...)) l.log.InfoContext(ctx, fmt.Sprintf(msg, args...))
} }
func (l *logger) Warn(ctx context.Context, msg string, args ...any) { func (l *logger) Warn(ctx context.Context, msg string, args ...any) {
l.log.WarnContext(ctx, msg, args...)
}
func (l *logger) Warnf(ctx context.Context, msg string, args ...any) {
l.log.WarnContext(ctx, fmt.Sprintf(msg, args...)) l.log.WarnContext(ctx, fmt.Sprintf(msg, args...))
} }
func (l *logger) Error(ctx context.Context, msg string, args ...any) { func (l *logger) Error(ctx context.Context, msg string, args ...any) {
l.log.ErrorContext(ctx, msg, args...)
}
func (l *logger) Errorf(ctx context.Context, msg string, args ...any) {
l.log.ErrorContext(ctx, fmt.Sprintf(msg, args...)) l.log.ErrorContext(ctx, fmt.Sprintf(msg, args...))
} }

View File

@ -3,7 +3,6 @@ package logger
import ( import (
"context" "context"
"log/slog" "log/slog"
"os"
) )
var ( var (
@ -11,56 +10,32 @@ var (
) )
func init() { func init() {
log = NewLogger(slog.New( log = NewLogger(slog.Default())
slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}),
))
} }
type Logger interface { type Logger interface {
Debug(ctx context.Context, format string, args ...any) //Structured context as loosely typed key-value pairs. Debug(ctx context.Context, format string, args ...any)
Debugf(ctx context.Context, format string, args ...any)
Info(ctx context.Context, format string, args ...any) Info(ctx context.Context, format string, args ...any)
Infof(ctx context.Context, format string, args ...any)
Warn(ctx context.Context, format string, args ...any) Warn(ctx context.Context, format string, args ...any)
Warnf(ctx context.Context, format string, args ...any)
Error(ctx context.Context, format string, args ...any) Error(ctx context.Context, format string, args ...any)
Errorf(ctx context.Context, format string, args ...any)
} }
func Debug(ctx context.Context, format string, args ...any) { func Debug(ctx context.Context, format string, args ...any) {
log.Debug(ctx, format, args...) log.Debug(ctx, format, args...)
} }
func Debugf(ctx context.Context, format string, args ...any) {
log.Debugf(ctx, format, args...)
}
func Info(ctx context.Context, format string, args ...any) { func Info(ctx context.Context, format string, args ...any) {
log.Info(ctx, format, args...) log.Debug(ctx, format, args...)
}
func Infof(ctx context.Context, format string, args ...any) {
log.Infof(ctx, format, args...)
} }
func Warn(ctx context.Context, format string, args ...any) { func Warn(ctx context.Context, format string, args ...any) {
log.Warn(ctx, format, args...) log.Debug(ctx, format, args...)
}
func Warnf(ctx context.Context, format string, args ...any) {
log.Warnf(ctx, format, args...)
} }
func Error(ctx context.Context, format string, args ...any) { func Error(ctx context.Context, format string, args ...any) {
log.Debug(ctx, format, args...) log.Debug(ctx, format, args...)
} }
func Errorf(ctx context.Context, format string, args ...any) {
log.Errorf(ctx, format, args...)
}
func Default() Logger { func Default() Logger {
return log return log
} }

View File

@ -31,16 +31,6 @@ 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
} }
@ -124,76 +114,6 @@ 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"`
@ -278,7 +198,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\"\xc6\x03\n" + "rest.proto\x12\x04aeus\x1a google/protobuf/descriptor.proto\"\xbc\x01\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" +
@ -286,18 +206,7 @@ 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\x12\x12\n" + "\x04rule\x18\a \x01(\tR\x04rule\"*\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,16 +21,6 @@ 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

@ -0,0 +1,120 @@
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()
}
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

@ -0,0 +1,271 @@
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

@ -0,0 +1,16 @@
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
}

69
tools/gen/main.go 100644
View File

@ -0,0 +1,69 @@
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,9 +6,7 @@ import (
"math" "math"
"net" "net"
"net/url" "net/url"
"os"
"runtime" "runtime"
"strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -32,7 +30,6 @@ type Server struct {
uri *url.URL uri *url.URL
exitFlag int32 exitFlag int32
middleware []middleware.Middleware middleware []middleware.Middleware
Logger logger.Logger
} }
func (svr *Server) Use(middlewares ...middleware.Middleware) { func (svr *Server) Use(middlewares ...middleware.Middleware) {
@ -135,7 +132,7 @@ func (s *Server) execute(ctx *Context, frame *Frame) (err error) {
func (svr *Server) nextSequence() int64 { func (svr *Server) nextSequence() int64 {
svr.sequenceLocker.Lock() svr.sequenceLocker.Lock()
defer svr.sequenceLocker.Unlock() defer svr.sequenceLocker.Unlock()
if svr.sequence == math.MaxInt64 { if svr.sequence >= math.MaxInt64 {
svr.sequence = 1 svr.sequence = 1
} }
svr.sequence++ svr.sequence++
@ -209,16 +206,10 @@ func (s *Server) serve() (err error) {
func (s *Server) Start(ctx context.Context) (err error) { func (s *Server) Start(ctx context.Context) (err error) {
s.ctx = ctx s.ctx = ctx
if s.opts.logger != nil {
s.Logger = s.opts.logger
}
if s.Logger == nil {
s.Logger = logger.Default()
}
if err = s.createListener(); err != nil { if err = s.createListener(); err != nil {
return return
} }
s.Logger.Infof(ctx, "cli server listen on: %s", s.uri.Host) s.opts.logger.Info(ctx, "cli server listen on: %s", s.uri.Host)
s.Handle("/help", "Display help information", func(ctx *Context) (err error) { s.Handle("/help", "Display help information", func(ctx *Context) (err error) {
return ctx.Success(s.router.String()) return ctx.Success(s.router.String())
}) })
@ -231,9 +222,7 @@ func (s *Server) Stop(ctx context.Context) (err error) {
return return
} }
if s.listener != nil { if s.listener != nil {
if err = s.listener.Close(); err != nil { err = s.listener.Close()
s.Logger.Warnf(ctx, "cli listener close error: %v", err)
}
} }
s.ctxMap.Range(func(key, value any) bool { s.ctxMap.Range(func(key, value any) bool {
if ctx, ok := value.(*Context); ok { if ctx, ok := value.(*Context); ok {
@ -241,7 +230,6 @@ func (s *Server) Stop(ctx context.Context) (err error) {
} }
return true return true
}) })
s.Logger.Info(ctx, "cli server stopped")
return return
} }
@ -250,12 +238,11 @@ func New(cbs ...Option) *Server {
opts: &options{ opts: &options{
network: "tcp", network: "tcp",
address: ":0", address: ":0",
logger: logger.Default(),
}, },
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,11 +2,8 @@ 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"
@ -25,7 +22,6 @@ type Server struct {
serve *grpc.Server serve *grpc.Server
listener net.Listener listener net.Listener
middlewares []middleware.Middleware middlewares []middleware.Middleware
Logger logger.Logger
} }
func (s *Server) createListener() (err error) { func (s *Server) createListener() (err error) {
@ -108,16 +104,11 @@ func (s *Server) Use(middlewares ...middleware.Middleware) {
func (s *Server) Start(ctx context.Context) (err error) { func (s *Server) Start(ctx context.Context) (err error) {
s.ctx = ctx s.ctx = ctx
if s.opts.logger != nil {
s.Logger = s.opts.logger
}
if s.Logger == nil {
s.Logger = logger.Default()
}
if err = s.createListener(); err != nil { if err = s.createListener(); err != nil {
return return
} }
s.Logger.Infof(ctx, "grpc server listen on: %s", s.uri.Host) s.opts.logger.Info(ctx, "grpc server listen on: %s", s.uri.Host)
reflection.Register(s.serve) reflection.Register(s.serve)
s.serve.Serve(s.listener) s.serve.Serve(s.listener)
return return
@ -136,7 +127,7 @@ func (s *Server) RegisterService(sd *grpc.ServiceDesc, ss any) {
func (s *Server) Stop(ctx context.Context) (err error) { func (s *Server) Stop(ctx context.Context) (err error) {
s.serve.GracefulStop() s.serve.GracefulStop()
s.Logger.Infof(s.ctx, "grpc server stopped") s.opts.logger.Info(s.ctx, "grpc server stopped")
return return
} }
@ -144,6 +135,8 @@ func New(cbs ...Option) *Server {
svr := &Server{ svr := &Server{
opts: &options{ opts: &options{
network: "tcp", network: "tcp",
logger: logger.Default(),
address: ":0",
grpcOpts: make([]grpc.ServerOption, 0, 10), grpcOpts: make([]grpc.ServerOption, 0, 10),
}, },
uri: &url.URL{ uri: &url.URL{
@ -151,8 +144,6 @@ 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,10 +22,6 @@ 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
} }
@ -42,14 +38,6 @@ 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

@ -1,153 +0,0 @@
package http
import (
"io/fs"
"net/http"
"os"
"path"
"strings"
"time"
)
type (
filesystem struct {
fs http.FileSystem
modtime time.Time
prefix string
indexFile string
denyDirectory bool
}
httpFile struct {
fp http.File
modtime time.Time
}
httpFileInfo struct {
name string
size int64
mode fs.FileMode
isDir bool
modtime time.Time
}
)
func (fi *httpFileInfo) Name() string {
return fi.name
}
func (fi *httpFileInfo) Size() int64 {
return fi.size
}
func (fi *httpFileInfo) Mode() fs.FileMode {
return fi.mode
}
func (fi *httpFileInfo) ModTime() time.Time {
return fi.modtime
}
func (fi *httpFileInfo) IsDir() bool {
return fi.isDir
}
func (fi *httpFileInfo) Sys() any {
return nil
}
func (file *httpFile) Close() error {
return file.fp.Close()
}
func (file *httpFile) Read(p []byte) (n int, err error) {
return file.fp.Read(p)
}
func (file *httpFile) Seek(offset int64, whence int) (int64, error) {
return file.fp.Seek(offset, whence)
}
func (file *httpFile) Readdir(count int) ([]fs.FileInfo, error) {
return file.fp.Readdir(count)
}
func (file *httpFile) Stat() (fs.FileInfo, error) {
fi, err := file.fp.Stat()
if err != nil {
return nil, err
}
return newFileInfo(fi, file.modtime), nil
}
func (fs *filesystem) DenyAccessDirectory() {
fs.denyDirectory = true
}
func (fs *filesystem) SetPrefix(prefix string) {
if prefix != "" {
if prefix[0] != '/' {
prefix = "/" + prefix
}
prefix = strings.TrimRight(prefix, "/")
fs.prefix = prefix
}
}
func (fs *filesystem) SetIndexFile(indexFile string) {
fs.indexFile = indexFile
}
func (fs *filesystem) Open(name string) (http.File, error) {
var (
needRetry bool
)
if name == "" || name == "/" {
needRetry = true
}
if fs.prefix != "" {
if !strings.HasPrefix(name, fs.prefix) {
name = path.Join(fs.prefix, name)
}
}
fp, err := fs.fs.Open(name)
if err != nil {
return nil, err
}
if fs.denyDirectory {
state, err := fp.Stat()
if err != nil {
return nil, err
}
if state.IsDir() {
if needRetry {
if fs.indexFile != "" {
return fs.Open(path.Join(name, fs.indexFile))
}
}
return nil, os.ErrPermission
}
}
return &httpFile{fp: fp, modtime: fs.modtime}, nil
}
func newFS(modtime time.Time, fs http.FileSystem) *filesystem {
return &filesystem{
fs: fs,
modtime: modtime,
}
}
func newFileInfo(fi fs.FileInfo, modtime time.Time) *httpFileInfo {
return &httpFileInfo{
name: fi.Name(),
size: fi.Size(),
mode: fi.Mode(),
isDir: fi.IsDir(),
modtime: modtime,
}
}
func FileSystem(s fs.FS) http.FileSystem {
return http.FS(s)
}

View File

@ -2,20 +2,11 @@ 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/filepath"
"slices"
"strconv"
"strings"
"sync" "sync"
"time"
"git.nobla.cn/golang/aeus/metadata" "git.nobla.cn/golang/aeus/metadata"
"git.nobla.cn/golang/aeus/middleware" "git.nobla.cn/golang/aeus/middleware"
@ -34,9 +25,7 @@ type Server struct {
engine *gin.Engine engine *gin.Engine
once sync.Once once sync.Once
listener net.Listener listener net.Listener
fs *filesystem
middlewares []middleware.Middleware middlewares []middleware.Middleware
Logger logger.Logger
} }
func (s *Server) Endpoint(ctx context.Context) (string, error) { func (s *Server) Endpoint(ctx context.Context) (string, error) {
@ -98,111 +87,10 @@ func (s *Server) Use(middlewares ...middleware.Middleware) {
s.middlewares = append(s.middlewares, middlewares...) s.middlewares = append(s.middlewares, middlewares...)
} }
func (s *Server) Handle(method string, uri string, handler http.HandlerFunc) {
s.engine.Handle(method, uri, func(ctx *gin.Context) {
handler(ctx.Writer, ctx.Request)
})
}
func (s *Server) Webroot(prefix string, fs http.FileSystem) {
s.fs = newFS(time.Now(), fs)
s.fs.SetPrefix(prefix)
s.fs.DenyAccessDirectory()
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()
}
func (s *Server) notFoundHandle(ctx *gin.Context) {
if s.fs != nil && ctx.Request.Method == http.MethodGet {
uri := path.Clean(ctx.Request.URL.Path)
if fp, err := s.fs.Open(uri); err == nil {
s.staticHandle(ctx, fp)
fp.Close()
}
}
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
@ -222,8 +110,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))
@ -255,12 +143,6 @@ func (s *Server) Start(ctx context.Context) (err error) {
Addr: s.opts.address, Addr: s.opts.address,
Handler: s.engine, Handler: s.engine,
} }
if s.opts.logger != nil {
s.Logger = s.opts.logger
}
if s.Logger == nil {
s.Logger = logger.Default()
}
s.ctx = ctx s.ctx = ctx
if s.opts.debug { if s.opts.debug {
s.engine.Handle(http.MethodGet, "/debug/pprof/", s.wrapHandle(pprof.Index)) s.engine.Handle(http.MethodGet, "/debug/pprof/", s.wrapHandle(pprof.Index))
@ -276,8 +158,7 @@ func (s *Server) Start(ctx context.Context) (err error) {
if err = s.createListener(); err != nil { if err = s.createListener(); err != nil {
return return
} }
s.engine.NoRoute(s.notFoundHandle) s.opts.logger.Info(ctx, "http server listen on: %s", s.uri.Host)
s.Logger.Infof(ctx, "http server listen on: %s", s.uri.Host)
if s.opts.certFile != "" && s.opts.keyFile != "" { if s.opts.certFile != "" && s.opts.keyFile != "" {
s.uri.Scheme = "https" s.uri.Scheme = "https"
err = s.serve.ServeTLS(s.listener, s.opts.certFile, s.opts.keyFile) err = s.serve.ServeTLS(s.listener, s.opts.certFile, s.opts.keyFile)
@ -292,7 +173,7 @@ func (s *Server) Start(ctx context.Context) (err error) {
func (s *Server) Stop(ctx context.Context) (err error) { func (s *Server) Stop(ctx context.Context) (err error) {
err = s.serve.Shutdown(ctx) err = s.serve.Shutdown(ctx)
s.Logger.Infof(ctx, "http server stopped") s.opts.logger.Info(ctx, "http server stopped")
return return
} }
@ -301,10 +182,10 @@ func New(cbs ...Option) *Server {
uri: &url.URL{Scheme: "http"}, uri: &url.URL{Scheme: "http"},
opts: &options{ opts: &options{
network: "tcp", network: "tcp",
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)
} }
@ -312,9 +193,6 @@ 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,14 +1,8 @@
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"
@ -31,7 +25,6 @@ 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)
@ -47,89 +40,12 @@ 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