Compare commits
37 Commits
Author | SHA1 | Date |
---|---|---|
|
b4ebbba30c | |
|
ac427e9134 | |
|
77ee2d8712 | |
|
129a9e768d | |
|
9dbae23345 | |
|
be8b56b897 | |
|
b28dbdb8c1 | |
|
7fff2f8a1c | |
|
bef2e35e41 | |
|
57c134b6ba | |
|
281b44f637 | |
|
0cd890fbb0 | |
|
634261a27c | |
|
a3835e2ead | |
|
5dac511cae | |
|
972eb004d2 | |
|
83afa05fa3 | |
|
df640f65fa | |
|
eb4c287da0 | |
|
809c45c301 | |
|
b91e28a512 | |
|
183f940f65 | |
|
2db8293160 | |
|
9cfa81115f | |
|
fbb08c6eb4 | |
|
c42b2317d6 | |
|
4b346afaec | |
|
6693cfe68f | |
|
b73084c1b5 | |
|
ffac331e3d | |
|
2877f751dc | |
|
2c5c83578c | |
|
d24f7efb0b | |
|
4199b81b5f | |
|
4114e5fcb0 | |
|
a995627a52 | |
|
302b90842b |
43
cmd/main.go
43
cmd/main.go
|
@ -4,8 +4,13 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"flag"
|
"flag"
|
||||||
|
httpkg "net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.nspix.com/golang/kos"
|
"git.nobla.cn/golang/kos/entry/cli"
|
||||||
|
"git.nobla.cn/golang/kos/entry/http"
|
||||||
|
|
||||||
|
"git.nobla.cn/golang/kos"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed web
|
//go:embed web
|
||||||
|
@ -14,8 +19,39 @@ var webDir embed.FS
|
||||||
type subServer struct {
|
type subServer struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type users struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
func (s *subServer) Start(ctx context.Context) (err error) {
|
func (s *subServer) Start(ctx context.Context) (err error) {
|
||||||
kos.Http().Embed("/ui/web", "web", webDir)
|
kos.Http().Root("/web", httpkg.FS(webDir))
|
||||||
|
kos.Http().Handle(httpkg.MethodGet, "/hello", func(ctx *http.Context) (err error) {
|
||||||
|
return ctx.Success("Hello World")
|
||||||
|
})
|
||||||
|
|
||||||
|
kos.Command().Handle("/test", "test command", func(ctx *cli.Context) (err error) {
|
||||||
|
return ctx.Success([][]string{
|
||||||
|
{"NAME", "AGE"},
|
||||||
|
{"SSS", "aaa"},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
kos.Command().Handle("/users", "test command", func(ctx *cli.Context) (err error) {
|
||||||
|
return ctx.Success([]*users{
|
||||||
|
{Name: "Zhan", Age: 10, Tags: []string{"a", "b"}},
|
||||||
|
{Name: "Lisi", Age: 15, Tags: []string{"c", "d"}},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
kos.Command().Handle("/ctx", "context test", func(ctx *cli.Context) (err error) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Context().Done():
|
||||||
|
case <-time.After(time.Second * 2):
|
||||||
|
}
|
||||||
|
return ctx.Success("OK")
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,9 +62,8 @@ func (s *subServer) Stop() (err error) {
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
svr := kos.Init(
|
svr := kos.Init(
|
||||||
kos.WithName("git.nspix.com/golang/test", "0.0.1"),
|
kos.WithName("git.nobla.cn/golang/test", "0.0.1"),
|
||||||
kos.WithServer(&subServer{}),
|
kos.WithServer(&subServer{}),
|
||||||
kos.WithDirectHttp(),
|
|
||||||
)
|
)
|
||||||
svr.Run()
|
svr.Run()
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<title>Document</title>
|
<title>Document</title>
|
||||||
<link rel="stylesheet" href="css/index.css">
|
<link rel="stylesheet" href="/css/index.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Hello</h1>
|
<h1>Hello</h1>
|
||||||
|
|
|
@ -5,6 +5,7 @@ const (
|
||||||
EnvAppVersion = "VOX_VERSION"
|
EnvAppVersion = "VOX_VERSION"
|
||||||
EnvAppPort = "VOX_PORT"
|
EnvAppPort = "VOX_PORT"
|
||||||
EnvAppAddress = "VOX_ADDRESS"
|
EnvAppAddress = "VOX_ADDRESS"
|
||||||
|
EnvAppDebug = "VOX_DEBUG"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.nobla.cn/golang/kos/util/env"
|
||||||
"github.com/peterh/liner"
|
"github.com/peterh/liner"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
@ -141,7 +142,7 @@ func (client *Client) completer(str string) (ss []string) {
|
||||||
)
|
)
|
||||||
ss = make([]string, 0)
|
ss = make([]string, 0)
|
||||||
seq = client.getSequence()
|
seq = client.getSequence()
|
||||||
if err = writeFrame(client.conn, newFrame(PacketTypeCompleter, FlagComplete, seq, []byte(str))); err != nil {
|
if err = writeFrame(client.conn, newFrame(PacketTypeCompleter, FlagComplete, seq, client.Timeout, []byte(str))); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
|
@ -166,10 +167,10 @@ func (client *Client) Execute(s string) (err error) {
|
||||||
}()
|
}()
|
||||||
go client.ioLoop(client.conn)
|
go client.ioLoop(client.conn)
|
||||||
seq = client.getSequence()
|
seq = client.getSequence()
|
||||||
if err = writeFrame(client.conn, newFrame(PacketTypeCommand, FlagComplete, seq, []byte(s))); err != nil {
|
if err = writeFrame(client.conn, newFrame(PacketTypeCommand, FlagComplete, seq, client.Timeout, []byte(s))); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
client.waitResponse(seq, time.Second*30)
|
client.waitResponse(seq, client.Timeout)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +186,7 @@ func (client *Client) Shell() (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = client.Close()
|
_ = client.Close()
|
||||||
}()
|
}()
|
||||||
if err = writeFrame(client.conn, newFrame(PacketTypeHandshake, FlagComplete, client.getSequence(), nil)); err != nil {
|
if err = writeFrame(client.conn, newFrame(PacketTypeHandshake, FlagComplete, client.getSequence(), client.Timeout, nil)); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go client.ioLoop(client.conn)
|
go client.ioLoop(client.conn)
|
||||||
|
@ -216,7 +217,7 @@ func (client *Client) Shell() (err error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seq = client.getSequence()
|
seq = client.getSequence()
|
||||||
if err = writeFrame(client.conn, newFrame(PacketTypeCommand, FlagComplete, seq, []byte(line))); err != nil {
|
if err = writeFrame(client.conn, newFrame(PacketTypeCommand, FlagComplete, seq, client.Timeout, []byte(line))); err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
client.liner.AppendHistory(line)
|
client.liner.AppendHistory(line)
|
||||||
|
@ -240,14 +241,22 @@ func (client *Client) Close() (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(ctx context.Context, addr string) *Client {
|
func NewClient(ctx context.Context, addr string) *Client {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
timeout time.Duration
|
||||||
|
)
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
duration := env.Get("VOX_TIMEOUT", "30s")
|
||||||
|
if timeout, err = time.ParseDuration(duration); err != nil {
|
||||||
|
timeout = time.Second * 30
|
||||||
|
}
|
||||||
return &Client{
|
return &Client{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
address: addr,
|
address: addr,
|
||||||
name: filepath.Base(os.Args[0]),
|
name: filepath.Base(os.Args[0]),
|
||||||
Timeout: time.Second * 30,
|
Timeout: timeout,
|
||||||
liner: liner.NewLiner(),
|
liner: liner.NewLiner(),
|
||||||
readyChan: make(chan struct{}, 1),
|
readyChan: make(chan struct{}, 1),
|
||||||
exitChan: make(chan struct{}),
|
exitChan: make(chan struct{}),
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Context struct {
|
type Context struct {
|
||||||
Id int64
|
Id int64
|
||||||
seq uint16
|
seq uint16
|
||||||
|
ctx context.Context
|
||||||
wc io.WriteCloser
|
wc io.WriteCloser
|
||||||
params map[string]string
|
params map[string]string
|
||||||
|
locker sync.RWMutex
|
||||||
|
variables map[string]any
|
||||||
args []string
|
args []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,8 +23,10 @@ func (ctx *Context) reset(id int64, wc io.WriteCloser) {
|
||||||
ctx.Id = id
|
ctx.Id = id
|
||||||
ctx.wc = wc
|
ctx.wc = wc
|
||||||
ctx.seq = 0
|
ctx.seq = 0
|
||||||
|
ctx.ctx = context.Background()
|
||||||
ctx.args = make([]string, 0)
|
ctx.args = make([]string, 0)
|
||||||
ctx.params = make(map[string]string)
|
ctx.params = make(map[string]string)
|
||||||
|
ctx.variables = make(map[string]any)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) setArgs(args []string) {
|
func (ctx *Context) setArgs(args []string) {
|
||||||
|
@ -34,6 +41,14 @@ func (ctx *Context) Bind(v any) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) setContext(c context.Context) {
|
||||||
|
ctx.ctx = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Context() context.Context {
|
||||||
|
return ctx.ctx
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx *Context) Argument(index int) string {
|
func (ctx *Context) Argument(index int) string {
|
||||||
if index >= len(ctx.args) || index < 0 {
|
if index >= len(ctx.args) || index < 0 {
|
||||||
return ""
|
return ""
|
||||||
|
@ -48,6 +63,22 @@ func (ctx *Context) Param(s string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) SetValue(name string, value any) {
|
||||||
|
ctx.locker.Lock()
|
||||||
|
if ctx.variables == nil {
|
||||||
|
ctx.variables = make(map[string]any)
|
||||||
|
}
|
||||||
|
ctx.variables[name] = value
|
||||||
|
ctx.locker.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) GetValue(name string) (val any, ok bool) {
|
||||||
|
ctx.locker.RLock()
|
||||||
|
defer ctx.locker.RUnlock()
|
||||||
|
val, ok = ctx.variables[name]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx *Context) Success(v any) (err error) {
|
func (ctx *Context) Success(v any) (err error) {
|
||||||
return ctx.send(responsePayload{Type: PacketTypeCommand, Data: v})
|
return ctx.send(responsePayload{Type: PacketTypeCommand, Data: v})
|
||||||
}
|
}
|
||||||
|
@ -93,12 +124,12 @@ __END:
|
||||||
chunkSize := math.MaxInt16 - 1
|
chunkSize := math.MaxInt16 - 1
|
||||||
n := len(buf) / chunkSize
|
n := len(buf) / chunkSize
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
if err = writeFrame(ctx.wc, newFrame(res.Type, FlagPortion, ctx.seq, buf[offset:chunkSize+offset])); err != nil {
|
if err = writeFrame(ctx.wc, newFrame(res.Type, FlagPortion, ctx.seq, 0, buf[offset:chunkSize+offset])); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
offset += chunkSize
|
offset += chunkSize
|
||||||
}
|
}
|
||||||
err = writeFrame(ctx.wc, newFrame(res.Type, FlagComplete, ctx.seq, buf[offset:]))
|
err = writeFrame(ctx.wc, newFrame(res.Type, FlagComplete, ctx.seq, 0, buf[offset:]))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ type (
|
||||||
Seq uint16 `json:"seq"`
|
Seq uint16 `json:"seq"`
|
||||||
Data []byte `json:"data"`
|
Data []byte `json:"data"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
|
Timeout int64 `json:"timeout"`
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -55,6 +56,9 @@ func readFrame(r io.Reader) (frame *Frame, err error) {
|
||||||
if err = binary.Read(r, binary.LittleEndian, &frame.Seq); err != nil {
|
if err = binary.Read(r, binary.LittleEndian, &frame.Seq); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err = binary.Read(r, binary.LittleEndian, &frame.Timeout); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err = binary.Read(r, binary.LittleEndian, &frame.Timestamp); err != nil {
|
if err = binary.Read(r, binary.LittleEndian, &frame.Timestamp); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -116,6 +120,9 @@ func writeFrame(w io.Writer, frame *Frame) (err error) {
|
||||||
if err = binary.Write(w, binary.LittleEndian, frame.Seq); err != nil {
|
if err = binary.Write(w, binary.LittleEndian, frame.Seq); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err = binary.Write(w, binary.LittleEndian, frame.Timeout); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err = binary.Write(w, binary.LittleEndian, frame.Timestamp); err != nil {
|
if err = binary.Write(w, binary.LittleEndian, frame.Timestamp); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -142,13 +149,14 @@ func writeFrame(w io.Writer, frame *Frame) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFrame(t, f byte, seq uint16, data []byte) *Frame {
|
func newFrame(t, f byte, seq uint16, timeout time.Duration, data []byte) *Frame {
|
||||||
return &Frame{
|
return &Frame{
|
||||||
Feature: Feature,
|
Feature: Feature,
|
||||||
Type: t,
|
Type: t,
|
||||||
Flag: f,
|
Flag: f,
|
||||||
Seq: seq,
|
Seq: seq,
|
||||||
Data: data,
|
Data: data,
|
||||||
|
Timeout: int64(timeout),
|
||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,9 @@ func (r *Router) Handle(path string, command Command) {
|
||||||
name = path
|
name = path
|
||||||
path = ""
|
path = ""
|
||||||
}
|
}
|
||||||
|
if name == "-" {
|
||||||
|
name = "app"
|
||||||
|
}
|
||||||
children := r.getChildren(name)
|
children := r.getChildren(name)
|
||||||
if children == nil {
|
if children == nil {
|
||||||
children = newRouter(name)
|
children = newRouter(name)
|
||||||
|
|
|
@ -4,7 +4,8 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.nspix.com/golang/kos/util/pool"
|
"git.nobla.cn/golang/kos/util/arrays"
|
||||||
|
"git.nobla.cn/golang/kos/util/pool"
|
||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -170,7 +171,7 @@ func serializeArray(val []any) (buf []byte, err error) {
|
||||||
if rv.Kind() == reflect.Array || rv.Kind() == reflect.Slice {
|
if rv.Kind() == reflect.Array || rv.Kind() == reflect.Slice {
|
||||||
row := make([]any, 0, rv.Len())
|
row := make([]any, 0, rv.Len())
|
||||||
for i := 0; i < rv.Len(); i++ {
|
for i := 0; i < rv.Len(); i++ {
|
||||||
if isNormalKind(rv.Index(i).Elem().Kind()) || rv.Index(i).Interface() == nil {
|
if isNormalKind(rv.Index(i).Kind()) || rv.Index(i).Interface() == nil {
|
||||||
row = append(row, rv.Index(i).Interface())
|
row = append(row, rv.Index(i).Interface())
|
||||||
} else {
|
} else {
|
||||||
goto __END
|
goto __END
|
||||||
|
@ -184,6 +185,7 @@ func serializeArray(val []any) (buf []byte, err error) {
|
||||||
}
|
}
|
||||||
if isStructElement {
|
if isStructElement {
|
||||||
vs = make([][]any, 0, len(val))
|
vs = make([][]any, 0, len(val))
|
||||||
|
indexes := make([]int, 0)
|
||||||
for i, v := range val {
|
for i, v := range val {
|
||||||
rv := reflect.Indirect(reflect.ValueOf(v))
|
rv := reflect.Indirect(reflect.ValueOf(v))
|
||||||
if rv.Kind() == reflect.Struct {
|
if rv.Kind() == reflect.Struct {
|
||||||
|
@ -191,17 +193,27 @@ func serializeArray(val []any) (buf []byte, err error) {
|
||||||
row := make([]any, 0, rv.Type().NumField())
|
row := make([]any, 0, rv.Type().NumField())
|
||||||
for j := 0; j < rv.Type().NumField(); j++ {
|
for j := 0; j < rv.Type().NumField(); j++ {
|
||||||
st := rv.Type().Field(j).Tag
|
st := rv.Type().Field(j).Tag
|
||||||
if columnName, ok = st.Lookup("name"); !ok {
|
if columnName, ok = st.Lookup("kos"); !ok {
|
||||||
columnName = strings.ToUpper(rv.Type().Field(j).Name)
|
columnName = strings.ToUpper(rv.Type().Field(j).Name)
|
||||||
|
} else {
|
||||||
|
if columnName == "-" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if !rv.Type().Field(j).IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
indexes = append(indexes, j)
|
||||||
row = append(row, columnName)
|
row = append(row, columnName)
|
||||||
}
|
}
|
||||||
vs = append(vs, row)
|
vs = append(vs, row)
|
||||||
}
|
}
|
||||||
row := make([]any, 0, rv.Type().NumField())
|
row := make([]any, 0, rv.Type().NumField())
|
||||||
for j := 0; j < rv.Type().NumField(); j++ {
|
for j := 0; j < rv.Type().NumField(); j++ {
|
||||||
|
if arrays.Exists(j, indexes) {
|
||||||
row = append(row, rv.Field(j).Interface())
|
row = append(row, rv.Field(j).Interface())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
vs = append(vs, row)
|
vs = append(vs, row)
|
||||||
} else {
|
} else {
|
||||||
goto __END
|
goto __END
|
||||||
|
|
|
@ -10,9 +10,10 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.nspix.com/golang/kos/util/env"
|
"git.nobla.cn/golang/kos/util/env"
|
||||||
"github.com/sourcegraph/conc"
|
"github.com/sourcegraph/conc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ type Server struct {
|
||||||
middleware []Middleware
|
middleware []Middleware
|
||||||
router *Router
|
router *Router
|
||||||
l net.Listener
|
l net.Listener
|
||||||
|
exitFlag int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Server) applyContext() *Context {
|
func (svr *Server) applyContext() *Context {
|
||||||
|
@ -44,9 +46,8 @@ func (svr *Server) releaseContext(ctx *Context) {
|
||||||
ctxPool.Put(ctx)
|
ctxPool.Put(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Server) handle(ctx *Context, frame *Frame) {
|
func (svr *Server) execute(ctx *Context, frame *Frame) (err error) {
|
||||||
var (
|
var (
|
||||||
err error
|
|
||||||
params map[string]string
|
params map[string]string
|
||||||
tokens []string
|
tokens []string
|
||||||
args []string
|
args []string
|
||||||
|
@ -54,6 +55,15 @@ func (svr *Server) handle(ctx *Context, frame *Frame) {
|
||||||
)
|
)
|
||||||
cmd := string(frame.Data)
|
cmd := string(frame.Data)
|
||||||
tokens = strings.Fields(cmd)
|
tokens = strings.Fields(cmd)
|
||||||
|
if frame.Timeout > 0 {
|
||||||
|
childCtx, cancelFunc := context.WithTimeout(svr.ctx, time.Duration(frame.Timeout))
|
||||||
|
ctx.setContext(childCtx)
|
||||||
|
defer func() {
|
||||||
|
cancelFunc()
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
ctx.setContext(svr.ctx)
|
||||||
|
}
|
||||||
if r, args, err = svr.router.Lookup(tokens); err != nil {
|
if r, args, err = svr.router.Lookup(tokens); err != nil {
|
||||||
if errors.Is(err, ErrNotFound) {
|
if errors.Is(err, ErrNotFound) {
|
||||||
err = ctx.Error(errNotFound, fmt.Sprintf("Command %s not found", cmd))
|
err = ctx.Error(errNotFound, fmt.Sprintf("Command %s not found", cmd))
|
||||||
|
@ -75,6 +85,7 @@ func (svr *Server) handle(ctx *Context, frame *Frame) {
|
||||||
ctx.setParam(params)
|
ctx.setParam(params)
|
||||||
err = r.command.Handle(ctx)
|
err = r.command.Handle(ctx)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Server) nextSequence() int64 {
|
func (svr *Server) nextSequence() int64 {
|
||||||
|
@ -130,7 +141,9 @@ func (svr *Server) process(conn net.Conn) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case PacketTypeCommand:
|
case PacketTypeCommand:
|
||||||
svr.handle(ctx, frame)
|
if err = svr.execute(ctx, frame); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -189,11 +202,17 @@ func (svr *Server) Serve(l net.Listener) (err error) {
|
||||||
return ctx.Success(svr.router.String())
|
return ctx.Success(svr.router.String())
|
||||||
})
|
})
|
||||||
svr.serve()
|
svr.serve()
|
||||||
|
atomic.StoreInt32(&svr.exitFlag, 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Server) Shutdown() (err error) {
|
func (svr *Server) Shutdown() (err error) {
|
||||||
|
if !atomic.CompareAndSwapInt32(&svr.exitFlag, 0, 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if svr.l != nil {
|
||||||
err = svr.l.Close()
|
err = svr.l.Close()
|
||||||
|
}
|
||||||
svr.ctxMap.Range(func(key, value any) bool {
|
svr.ctxMap.Range(func(key, value any) bool {
|
||||||
if ctx, ok := value.(*Context); ok {
|
if ctx, ok := value.(*Context); ok {
|
||||||
err = ctx.Close()
|
err = ctx.Close()
|
||||||
|
|
|
@ -14,32 +14,63 @@ var (
|
||||||
defaultBinder = &DefaultBinder{}
|
defaultBinder = &DefaultBinder{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
realIPHeaders = []string{
|
||||||
|
"Cf-Connecting-Ip",
|
||||||
|
"True-Client-IP",
|
||||||
|
"X-Forwarded-For",
|
||||||
|
"X-Real-Ip",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
type Context struct {
|
type Context struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
req *http.Request
|
req *http.Request
|
||||||
res http.ResponseWriter
|
res http.ResponseWriter
|
||||||
params map[string]string
|
params map[string]string
|
||||||
|
user *Userinfo
|
||||||
statusCode int
|
statusCode int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) reset(req *http.Request, res http.ResponseWriter, ps map[string]string) {
|
func (ctx *Context) reset(req *http.Request, res http.ResponseWriter, ps map[string]string) {
|
||||||
ctx.statusCode = http.StatusOK
|
ctx.statusCode = http.StatusOK
|
||||||
|
ctx.user = nil
|
||||||
ctx.req, ctx.res, ctx.params = req, res, ps
|
ctx.req, ctx.res, ctx.params = req, res, ps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) User() *Userinfo {
|
||||||
|
return ctx.user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) SetUser(ui *Userinfo) {
|
||||||
|
ctx.user = ui
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx *Context) RealIp() string {
|
func (ctx *Context) RealIp() string {
|
||||||
if ip := ctx.Request().Header.Get("X-Forwarded-For"); ip != "" {
|
var (
|
||||||
i := strings.IndexAny(ip, ",")
|
s string
|
||||||
if i > 0 {
|
pos int
|
||||||
return strings.TrimSpace(ip[:i])
|
ipaddr string
|
||||||
|
)
|
||||||
|
for _, h := range realIPHeaders {
|
||||||
|
if ipaddr = ctx.Request().Header.Get(h); ipaddr != "" {
|
||||||
|
goto __end
|
||||||
}
|
}
|
||||||
return ip
|
|
||||||
}
|
}
|
||||||
if ip := ctx.Request().Header.Get("X-Real-IP"); ip != "" {
|
ipaddr, _, _ = net.SplitHostPort(ctx.Request().RemoteAddr)
|
||||||
return ip
|
__end:
|
||||||
|
for {
|
||||||
|
if pos = strings.IndexByte(ipaddr, ','); pos > -1 {
|
||||||
|
s = strings.TrimSpace(ipaddr[:pos])
|
||||||
|
if netAddr := net.ParseIP(s); netAddr != nil && !netAddr.IsPrivate() {
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
ra, _, _ := net.SplitHostPort(ctx.Request().RemoteAddr)
|
ipaddr = ipaddr[pos+1:]
|
||||||
return ra
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(ipaddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) Request() *http.Request {
|
func (ctx *Context) Request() *http.Request {
|
||||||
|
@ -62,23 +93,36 @@ func (ctx *Context) Bind(v any) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) Query(k string) string {
|
func (ctx *Context) Query(k string) string {
|
||||||
|
qs := ctx.Request().URL.Query()
|
||||||
|
if qs == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return qs.Get(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Form(k string) string {
|
||||||
return ctx.Request().FormValue(k)
|
return ctx.Request().FormValue(k)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) Param(k string) string {
|
func (ctx *Context) Param(k string) string {
|
||||||
var (
|
var (
|
||||||
ok bool
|
ok bool
|
||||||
v string
|
s string
|
||||||
)
|
)
|
||||||
if v, ok = ctx.params[k]; ok {
|
if s, ok = ctx.params[k]; ok {
|
||||||
return v
|
return s
|
||||||
}
|
}
|
||||||
return ctx.Request().FormValue(k)
|
s = ctx.Query(k)
|
||||||
|
if s == "" {
|
||||||
|
s = ctx.Form(k)
|
||||||
|
}
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) send(res responsePayload) (err error) {
|
func (ctx *Context) json(res responsePayload) (err error) {
|
||||||
ctx.Response().Header().Set("Content-Type", "application/json")
|
ctx.Response().Header().Set("Content-Type", "application/json")
|
||||||
encoder := json.NewEncoder(ctx.Response())
|
encoder := json.NewEncoder(ctx.Response())
|
||||||
|
encoder.SetEscapeHTML(false)
|
||||||
if strings.HasPrefix(ctx.Request().Header.Get("User-Agent"), "curl") {
|
if strings.HasPrefix(ctx.Request().Header.Get("User-Agent"), "curl") {
|
||||||
encoder.SetIndent("", "\t")
|
encoder.SetIndent("", "\t")
|
||||||
}
|
}
|
||||||
|
@ -86,7 +130,7 @@ func (ctx *Context) send(res responsePayload) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) Success(v any) (err error) {
|
func (ctx *Context) Success(v any) (err error) {
|
||||||
return ctx.send(responsePayload{Data: v})
|
return ctx.json(responsePayload{Data: v})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) Status(code int) {
|
func (ctx *Context) Status(code int) {
|
||||||
|
@ -94,12 +138,12 @@ func (ctx *Context) Status(code int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) Error(code int, reason string) (err error) {
|
func (ctx *Context) Error(code int, reason string) (err error) {
|
||||||
return ctx.send(responsePayload{Code: code, Reason: reason})
|
return ctx.json(responsePayload{Code: code, Reason: reason})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) Redirect(url string, code int) {
|
func (ctx *Context) Redirect(url string, code int) {
|
||||||
if code != http.StatusFound && code != http.StatusMovedPermanently {
|
if code != http.StatusFound && code != http.StatusMovedPermanently {
|
||||||
code = http.StatusMovedPermanently
|
code = http.StatusFound
|
||||||
}
|
}
|
||||||
http.Redirect(ctx.Response(), ctx.Request(), url, code)
|
http.Redirect(ctx.Response(), ctx.Request(), url, code)
|
||||||
}
|
}
|
||||||
|
@ -108,6 +152,44 @@ func (ctx *Context) SetCookie(cookie *http.Cookie) {
|
||||||
http.SetCookie(ctx.Response(), cookie)
|
http.SetCookie(ctx.Response(), cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) GetCookie(name string) (*http.Cookie, error) {
|
||||||
|
return ctx.Request().Cookie(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) DeleteCookie(name string) {
|
||||||
|
cookie, err := ctx.GetCookie(name)
|
||||||
|
if err == nil {
|
||||||
|
cookie.MaxAge = -1
|
||||||
|
ctx.SetCookie(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) SetCookieValue(name, value, domain string) {
|
||||||
|
if domain == "" {
|
||||||
|
domain = ctx.Request().URL.Hostname()
|
||||||
|
}
|
||||||
|
if name == "" || value == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.SetCookie(&http.Cookie{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Path: "/",
|
||||||
|
Domain: domain,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) GetCookieValue(name string) string {
|
||||||
|
if name == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
cookie, err := ctx.GetCookie(name)
|
||||||
|
if err == nil {
|
||||||
|
return cookie.Value
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx *Context) SendFile(filename string) (err error) {
|
func (ctx *Context) SendFile(filename string) (err error) {
|
||||||
var (
|
var (
|
||||||
fi os.FileInfo
|
fi os.FileInfo
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContext_RealIp(t *testing.T) {
|
||||||
|
var (
|
||||||
|
pos int
|
||||||
|
s string
|
||||||
|
)
|
||||||
|
ipaddr := "192.168.6.76, 192.168.6.76, 116.24.65.173,192.168.6.76"
|
||||||
|
for {
|
||||||
|
if pos = strings.IndexByte(ipaddr, ','); pos > -1 {
|
||||||
|
s = strings.TrimSpace(ipaddr[:pos])
|
||||||
|
if netip := net.ParseIP(s); netip != nil && !netip.IsPrivate() {
|
||||||
|
t.Log(s)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ipaddr = ipaddr[pos+1:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Log(ipaddr)
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import "git.nobla.cn/golang/kos/pkg/types"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrAccessDenied = types.ErrAccessDenied //拒绝访问
|
||||||
|
ErrPermissionDenied = types.ErrPermissionDenied //没有权限
|
||||||
|
ErrIllegalRequest = types.ErrIllegalRequest //非法请求
|
||||||
|
ErrInvalidPayload = types.ErrInvalidPayload //请求数据无效
|
||||||
|
ErrResourceCreate = types.ErrResourceCreate //资源创建失败
|
||||||
|
ErrResourceUpdate = types.ErrResourceUpdate //资源更新失败
|
||||||
|
ErrResourceDelete = types.ErrResourceDelete //资源删除失败
|
||||||
|
ErrResourceNotFound = types.ErrResourceNotFound //资源未找到
|
||||||
|
ErrResourceEmpty = types.ErrResourceEmpty //资源为空
|
||||||
|
ErrResourceExpired = types.ErrResourceExpired //资源已失效
|
||||||
|
ErrResourceUnavailable = types.ErrResourceUnavailable //资源无法使用
|
||||||
|
ErrResourceLocked = types.ErrResourceLocked //资源已被锁定
|
||||||
|
ErrServerUnreachable = types.ErrServerUnreachable //服务不可用
|
||||||
|
ErrTemporaryUnavailable = types.ErrTemporaryUnavailable //临时性失败
|
||||||
|
ErrFatal = types.ErrFatal //致命错误
|
||||||
|
)
|
|
@ -0,0 +1,149 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
FS struct {
|
||||||
|
fs http.FileSystem
|
||||||
|
modtime time.Time
|
||||||
|
prefix string
|
||||||
|
indexFile string
|
||||||
|
denyDirectory bool
|
||||||
|
}
|
||||||
|
|
||||||
|
File struct {
|
||||||
|
fp http.File
|
||||||
|
modtime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInfo struct {
|
||||||
|
name string
|
||||||
|
size int64
|
||||||
|
mode fs.FileMode
|
||||||
|
isDir bool
|
||||||
|
modtime time.Time
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (fi *FileInfo) Name() string {
|
||||||
|
return fi.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi *FileInfo) Size() int64 {
|
||||||
|
return fi.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi *FileInfo) Mode() fs.FileMode {
|
||||||
|
return fi.mode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi *FileInfo) ModTime() time.Time {
|
||||||
|
return fi.modtime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi *FileInfo) IsDir() bool {
|
||||||
|
return fi.isDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi *FileInfo) Sys() any {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (file *File) Close() error {
|
||||||
|
return file.fp.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (file *File) Read(p []byte) (n int, err error) {
|
||||||
|
return file.fp.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (file *File) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
return file.fp.Seek(offset, whence)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (file *File) Readdir(count int) ([]fs.FileInfo, error) {
|
||||||
|
return file.fp.Readdir(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (file *File) Stat() (fs.FileInfo, error) {
|
||||||
|
fi, err := file.fp.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newFileInfo(fi, file.modtime), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) DenyAccessDirectory() {
|
||||||
|
fs.denyDirectory = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) SetPrefix(prefix string) {
|
||||||
|
if prefix != "" {
|
||||||
|
if prefix[0] != '/' {
|
||||||
|
prefix = "/" + prefix
|
||||||
|
}
|
||||||
|
prefix = strings.TrimRight(prefix, "/")
|
||||||
|
fs.prefix = prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) SetIndexFile(indexFile string) {
|
||||||
|
fs.indexFile = indexFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) 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 &File{fp: fp, modtime: fs.modtime}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFS(modtime time.Time, fs http.FileSystem) *FS {
|
||||||
|
return &FS{
|
||||||
|
fs: fs,
|
||||||
|
modtime: modtime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileInfo(fi fs.FileInfo, modtime time.Time) *FileInfo {
|
||||||
|
return &FileInfo{
|
||||||
|
name: fi.Name(),
|
||||||
|
size: fi.Size(),
|
||||||
|
mode: fi.Mode(),
|
||||||
|
isDir: fi.IsDir(),
|
||||||
|
modtime: modtime,
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,12 +3,14 @@ package http
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"git.nspix.com/golang/kos/entry/http/router"
|
"git.nobla.cn/golang/kos/entry/http/router"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -20,7 +22,12 @@ type Server struct {
|
||||||
serve *http.Server
|
serve *http.Server
|
||||||
router *router.Router
|
router *router.Router
|
||||||
middleware []Middleware
|
middleware []Middleware
|
||||||
|
uptime time.Time
|
||||||
|
enableDocumentRoot bool
|
||||||
|
fileSystem http.FileSystem
|
||||||
|
beforeRequests []HandleFunc
|
||||||
anyRequests map[string]http.Handler
|
anyRequests map[string]http.Handler
|
||||||
|
exitFlag int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Server) applyContext() *Context {
|
func (svr *Server) applyContext() *Context {
|
||||||
|
@ -39,14 +46,23 @@ func (svr *Server) releaseContext(ctx *Context) {
|
||||||
func (svr *Server) wrapHandle(cb HandleFunc, middleware ...Middleware) router.Handle {
|
func (svr *Server) wrapHandle(cb HandleFunc, middleware ...Middleware) router.Handle {
|
||||||
return func(writer http.ResponseWriter, request *http.Request, params router.Params) {
|
return func(writer http.ResponseWriter, request *http.Request, params router.Params) {
|
||||||
ctx := svr.applyContext()
|
ctx := svr.applyContext()
|
||||||
|
ps := make(map[string]string, 4)
|
||||||
defer func() {
|
defer func() {
|
||||||
svr.releaseContext(ctx)
|
svr.releaseContext(ctx)
|
||||||
|
ps = make(map[string]string, 0)
|
||||||
}()
|
}()
|
||||||
ps := make(map[string]string)
|
|
||||||
for _, v := range params {
|
for _, v := range params {
|
||||||
ps[v.Key] = v.Value
|
ps[v.Key] = v.Value
|
||||||
}
|
}
|
||||||
ctx.reset(request, writer, ps)
|
ctx.reset(request, writer, ps)
|
||||||
|
if len(svr.beforeRequests) > 0 {
|
||||||
|
for i := len(svr.beforeRequests) - 1; i >= 0; i-- {
|
||||||
|
if err := svr.beforeRequests[i](ctx); err != nil {
|
||||||
|
ctx.Status(http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for i := len(svr.middleware) - 1; i >= 0; i-- {
|
for i := len(svr.middleware) - 1; i >= 0; i-- {
|
||||||
cb = svr.middleware[i](cb)
|
cb = svr.middleware[i](cb)
|
||||||
}
|
}
|
||||||
|
@ -59,6 +75,10 @@ func (svr *Server) wrapHandle(cb HandleFunc, middleware ...Middleware) router.Ha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svr *Server) Before(cb ...HandleFunc) {
|
||||||
|
svr.beforeRequests = append(svr.beforeRequests, cb...)
|
||||||
|
}
|
||||||
|
|
||||||
func (svr *Server) Use(middleware ...Middleware) {
|
func (svr *Server) Use(middleware ...Middleware) {
|
||||||
svr.middleware = append(svr.middleware, middleware...)
|
svr.middleware = append(svr.middleware, middleware...)
|
||||||
}
|
}
|
||||||
|
@ -89,6 +109,15 @@ func (svr *Server) Group(prefix string, routes []Route, middleware ...Middleware
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svr *Server) Root(prefix string, fs http.FileSystem) {
|
||||||
|
svr.enableDocumentRoot = true
|
||||||
|
s := newFS(svr.uptime, fs)
|
||||||
|
s.SetPrefix(prefix)
|
||||||
|
s.DenyAccessDirectory()
|
||||||
|
s.SetIndexFile("/index.html")
|
||||||
|
svr.fileSystem = s
|
||||||
|
}
|
||||||
|
|
||||||
func (svr *Server) Embed(prefix string, root string, embedFs embed.FS) {
|
func (svr *Server) Embed(prefix string, root string, embedFs embed.FS) {
|
||||||
routePath := prefix
|
routePath := prefix
|
||||||
if !strings.HasSuffix(routePath, "/*filepath") {
|
if !strings.HasSuffix(routePath, "/*filepath") {
|
||||||
|
@ -115,7 +144,7 @@ func (svr *Server) Embed(prefix string, root string, embedFs embed.FS) {
|
||||||
filename = "/" + filename
|
filename = "/" + filename
|
||||||
}
|
}
|
||||||
ctx.Request().URL.Path = filename
|
ctx.Request().URL.Path = filename
|
||||||
http.FileServer(httpFs).ServeHTTP(ctx.Response(), ctx.Request())
|
http.FileServer(newFS(svr.uptime, httpFs)).ServeHTTP(ctx.Response(), ctx.Request())
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -157,12 +186,24 @@ func (svr *Server) handleRequest(res http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
func (svr *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
file http.File
|
||||||
|
)
|
||||||
for prefix, handle := range svr.anyRequests {
|
for prefix, handle := range svr.anyRequests {
|
||||||
if strings.HasPrefix(request.URL.Path, prefix) {
|
if strings.HasPrefix(request.URL.Path, prefix) {
|
||||||
handle.ServeHTTP(writer, request)
|
handle.ServeHTTP(writer, request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if svr.enableDocumentRoot && request.Method == http.MethodGet {
|
||||||
|
uri := path.Clean(request.URL.Path)
|
||||||
|
if file, err = svr.fileSystem.Open(uri); err == nil {
|
||||||
|
http.ServeContent(writer, request, path.Base(uri), svr.uptime, file)
|
||||||
|
err = file.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
switch request.Method {
|
switch request.Method {
|
||||||
case http.MethodOptions:
|
case http.MethodOptions:
|
||||||
svr.handleOption(writer, request)
|
svr.handleOption(writer, request)
|
||||||
|
@ -177,10 +218,14 @@ func (svr *Server) Serve(l net.Listener) (err error) {
|
||||||
}
|
}
|
||||||
svr.router.NotFound = NotFound{}
|
svr.router.NotFound = NotFound{}
|
||||||
svr.router.MethodNotAllowed = NotAllowed{}
|
svr.router.MethodNotAllowed = NotAllowed{}
|
||||||
|
atomic.StoreInt32(&svr.exitFlag, 0)
|
||||||
return svr.serve.Serve(l)
|
return svr.serve.Serve(l)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Server) Shutdown() (err error) {
|
func (svr *Server) Shutdown() (err error) {
|
||||||
|
if !atomic.CompareAndSwapInt32(&svr.exitFlag, 0, 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if svr.serve != nil {
|
if svr.serve != nil {
|
||||||
err = svr.serve.Shutdown(svr.ctx)
|
err = svr.serve.Shutdown(svr.ctx)
|
||||||
}
|
}
|
||||||
|
@ -190,7 +235,9 @@ func (svr *Server) Shutdown() (err error) {
|
||||||
func New(ctx context.Context) *Server {
|
func New(ctx context.Context) *Server {
|
||||||
svr := &Server{
|
svr := &Server{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
|
uptime: time.Now(),
|
||||||
router: router.New(),
|
router: router.New(),
|
||||||
|
beforeRequests: make([]HandleFunc, 0, 10),
|
||||||
anyRequests: make(map[string]http.Handler),
|
anyRequests: make(map[string]http.Handler),
|
||||||
middleware: make([]Middleware, 0, 10),
|
middleware: make([]Middleware, 0, 10),
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
type Userinfo struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
variables map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Userinfo) Set(k, v string) {
|
||||||
|
if ui.variables == nil {
|
||||||
|
ui.variables = make(map[string]string)
|
||||||
|
}
|
||||||
|
ui.variables[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Userinfo) Get(k string) string {
|
||||||
|
if ui.variables == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ui.variables[k]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Userinfo) Reset(id, name string) {
|
||||||
|
ui.ID = id
|
||||||
|
ui.Name = name
|
||||||
|
// clear the variables
|
||||||
|
for k, _ := range ui.variables {
|
||||||
|
delete(ui.variables, k)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestUserinfo_Set(t *testing.T) {
|
||||||
|
ui := &Userinfo{}
|
||||||
|
ui.Set("name", "xxx")
|
||||||
|
ui.Set("lost", "xxx")
|
||||||
|
if ui.Get("lost") != "xxx" {
|
||||||
|
t.Error("error")
|
||||||
|
}
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -1,4 +1,4 @@
|
||||||
module git.nspix.com/golang/kos
|
module git.nobla.cn/golang/kos
|
||||||
|
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
|
|
72
instance.go
72
instance.go
|
@ -1,43 +1,83 @@
|
||||||
package kos
|
package kos
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.nspix.com/golang/kos/entry/cli"
|
"git.nobla.cn/golang/kos/entry/cli"
|
||||||
"git.nspix.com/golang/kos/entry/http"
|
"git.nobla.cn/golang/kos/entry/http"
|
||||||
|
_ "git.nobla.cn/golang/kos/pkg/request"
|
||||||
|
_ "git.nobla.cn/golang/kos/util/arrays"
|
||||||
|
_ "git.nobla.cn/golang/kos/util/bs"
|
||||||
|
_ "git.nobla.cn/golang/kos/util/fetch"
|
||||||
|
_ "git.nobla.cn/golang/kos/util/humanize"
|
||||||
|
_ "git.nobla.cn/golang/kos/util/random"
|
||||||
|
_ "git.nobla.cn/golang/kos/util/reflection"
|
||||||
|
_ "git.nobla.cn/golang/kos/util/sys"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
once sync.Once
|
once sync.Once
|
||||||
std *application
|
app Application
|
||||||
)
|
)
|
||||||
|
|
||||||
func initApplication(cbs ...Option) {
|
func initialization(cbs ...Option) {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
std = New(cbs...)
|
app = New(cbs...)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Init(cbs ...Option) *application {
|
func Init(cbs ...Option) Application {
|
||||||
initApplication(cbs...)
|
initialization(cbs...)
|
||||||
return std
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
func Name() string {
|
||||||
|
initialization()
|
||||||
|
return app.Info().Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShortName() string {
|
||||||
|
initialization()
|
||||||
|
if entry, ok := app.(*application); ok {
|
||||||
|
return entry.opts.ShortName()
|
||||||
|
}
|
||||||
|
return app.Info().Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func Version() string {
|
||||||
|
initialization()
|
||||||
|
return app.Info().Version
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debug(args ...any) bool {
|
||||||
|
initialization()
|
||||||
|
if entry, ok := app.(*application); ok {
|
||||||
|
if len(args) <= 0 {
|
||||||
|
return entry.opts.EnableDebug
|
||||||
|
}
|
||||||
|
if b, ok := args[0].(bool); ok {
|
||||||
|
entry.opts.EnableDebug = b
|
||||||
|
}
|
||||||
|
return entry.opts.EnableDebug
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func Node() *Info {
|
func Node() *Info {
|
||||||
initApplication()
|
initialization()
|
||||||
return std.Info()
|
return app.Info()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Http() *http.Server {
|
func Http() *http.Server {
|
||||||
initApplication()
|
initialization()
|
||||||
return std.Http()
|
return app.Http()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Command() *cli.Server {
|
func Command() *cli.Server {
|
||||||
initApplication()
|
initialization()
|
||||||
return std.Command()
|
return app.Command()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Handle(method string, cb HandleFunc) {
|
func Handle(method string, cb HandleFunc) {
|
||||||
initApplication()
|
initialization()
|
||||||
std.Handle(method, cb)
|
app.Handle(method, cb)
|
||||||
}
|
}
|
||||||
|
|
54
options.go
54
options.go
|
@ -2,25 +2,27 @@ package kos
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"git.nspix.com/golang/kos/util/env"
|
"git.nobla.cn/golang/kos/util/env"
|
||||||
"git.nspix.com/golang/kos/util/ip"
|
"git.nobla.cn/golang/kos/util/ip"
|
||||||
"git.nspix.com/golang/kos/util/sys"
|
"git.nobla.cn/golang/kos/util/sys"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Options struct {
|
Options struct {
|
||||||
Name string
|
Name string //名称
|
||||||
Version string
|
Version string //版本号
|
||||||
Address string
|
Address string //绑定地址
|
||||||
Port int
|
Port int //端口
|
||||||
EnableDebug bool //开启调试模式
|
EnableDebug bool //开启调试模式
|
||||||
|
DisableGateway bool //禁用HTTP和COMMAND入口
|
||||||
DisableHttp bool //禁用HTTP入口
|
DisableHttp bool //禁用HTTP入口
|
||||||
EnableDirectHttp bool //启用HTTP直连模式
|
EnableDirectHttp bool //启用HTTP直连模式
|
||||||
DisableCommand bool //禁用命令行入口
|
DisableCommand bool //禁用COMMAND入口
|
||||||
EnableDirectCommand bool //启用命令行直连模式
|
EnableDirectCommand bool //启用COMMAND直连模式
|
||||||
DisableStateApi bool //禁用系统状态接口
|
DisableStateApi bool //禁用系统状态接口
|
||||||
Metadata map[string]string //原数据
|
Metadata map[string]string //原数据
|
||||||
Context context.Context
|
Context context.Context
|
||||||
|
@ -30,6 +32,12 @@ type (
|
||||||
}
|
}
|
||||||
|
|
||||||
Option func(o *Options)
|
Option func(o *Options)
|
||||||
|
|
||||||
|
HandleOptions struct {
|
||||||
|
description string
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleOption func(o *HandleOptions)
|
||||||
)
|
)
|
||||||
|
|
||||||
func (o *Options) ShortName() string {
|
func (o *Options) ShortName() string {
|
||||||
|
@ -44,6 +52,12 @@ func (o *Options) ShortName() string {
|
||||||
return o.shortName
|
return o.shortName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithHandleDescription(s string) HandleOption {
|
||||||
|
return func(o *HandleOptions) {
|
||||||
|
o.description = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithName(name string, version string) Option {
|
func WithName(name string, version string) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.Name = name
|
o.Name = name
|
||||||
|
@ -51,6 +65,12 @@ func WithName(name string, version string) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithoutGateway() Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.DisableGateway = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithPort(port int) Option {
|
func WithPort(port int) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.Port = port
|
o.Port = port
|
||||||
|
@ -83,7 +103,7 @@ func WithDirectCommand() Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOptions() *Options {
|
func NewOptions(cbs ...Option) *Options {
|
||||||
opts := &Options{
|
opts := &Options{
|
||||||
Name: env.Get(EnvAppName, sys.Hostname()),
|
Name: env.Get(EnvAppName, sys.Hostname()),
|
||||||
Version: env.Get(EnvAppVersion, "0.0.1"),
|
Version: env.Get(EnvAppVersion, "0.0.1"),
|
||||||
|
@ -92,6 +112,18 @@ func NewOptions() *Options {
|
||||||
Signals: []os.Signal{syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL},
|
Signals: []os.Signal{syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL},
|
||||||
}
|
}
|
||||||
opts.Port = int(env.Integer(18080, EnvAppPort, "HTTP_PORT", "KOS_PORT"))
|
opts.Port = int(env.Integer(18080, EnvAppPort, "HTTP_PORT", "KOS_PORT"))
|
||||||
opts.Address = env.Get(EnvAppAddress, ip.Internal())
|
opts.Address = env.Getter(ip.Internal(), EnvAppAddress, "KOS_ADDRESS")
|
||||||
|
opts.EnableDebug, _ = strconv.ParseBool(env.Getter("false", EnvAppDebug, "KOS_DEBUG"))
|
||||||
|
for _, cb := range cbs {
|
||||||
|
cb(opts)
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHandleOptions(cbs ...HandleOption) *HandleOptions {
|
||||||
|
opts := &HandleOptions{}
|
||||||
|
for _, cb := range cbs {
|
||||||
|
cb(opts)
|
||||||
|
}
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,14 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
LoadFunc func(ctx context.Context) ([]byte, error)
|
||||||
|
)
|
||||||
|
|
||||||
type Cache interface {
|
type Cache interface {
|
||||||
Set(ctx context.Context, key string, value any)
|
Set(ctx context.Context, key string, buf []byte)
|
||||||
SetEx(ctx context.Context, key string, value any, expire time.Duration)
|
SetEx(ctx context.Context, key string, buf []byte, expire time.Duration)
|
||||||
Get(ctx context.Context, key string) (value any, ok bool)
|
Get(ctx context.Context, key string) (buf []byte, ok bool)
|
||||||
|
Try(ctx context.Context, key string, cb LoadFunc) (buf []byte, err error)
|
||||||
Del(ctx context.Context, key string)
|
Del(ctx context.Context, key string)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,18 +23,57 @@ func GetCache() Cache {
|
||||||
return std
|
return std
|
||||||
}
|
}
|
||||||
|
|
||||||
func Set(ctx context.Context, key string, value any) {
|
// Set 设置缓存数据
|
||||||
std.Set(ctx, key, value)
|
func Set(ctx context.Context, key string, buf []byte) {
|
||||||
|
std.Set(ctx, key, buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetEx(ctx context.Context, key string, value any, expire time.Duration) {
|
// SetEx 设置一个有效时间的缓存数据
|
||||||
std.SetEx(ctx, key, value, expire)
|
func SetEx(ctx context.Context, key string, buf []byte, expire time.Duration) {
|
||||||
|
std.SetEx(ctx, key, buf, expire)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get(ctx context.Context, key string) (value any, ok bool) {
|
// Try 尝试获取缓存数据,获取不到就设置
|
||||||
|
func Try(ctx context.Context, key string, cb LoadFunc) (buf []byte, err error) {
|
||||||
|
return std.Try(ctx, key, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取缓存数据
|
||||||
|
func Get(ctx context.Context, key string) (buf []byte, ok bool) {
|
||||||
return std.Get(ctx, key)
|
return std.Get(ctx, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Del 删除缓存数据
|
||||||
func Del(ctx context.Context, key string) {
|
func Del(ctx context.Context, key string) {
|
||||||
std.Del(ctx, key)
|
std.Del(ctx, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store 存储缓存数据
|
||||||
|
func Store(ctx context.Context, key string, val any) (err error) {
|
||||||
|
return StoreEx(ctx, key, val, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreEx 存储缓存数据
|
||||||
|
func StoreEx(ctx context.Context, key string, val any, expire time.Duration) (err error) {
|
||||||
|
var (
|
||||||
|
buf []byte
|
||||||
|
)
|
||||||
|
if buf, err = json.Marshal(val); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
SetEx(ctx, key, buf, expire)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load 加载指定的缓存数据
|
||||||
|
func Load(ctx context.Context, key string, val any) (err error) {
|
||||||
|
var (
|
||||||
|
ok bool
|
||||||
|
buf []byte
|
||||||
|
)
|
||||||
|
if buf, ok = Get(ctx, key); !ok {
|
||||||
|
return os.ErrNotExist
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(buf, val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
@ -2,24 +2,66 @@ package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"git.nobla.cn/golang/kos/util/env"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
memCacheDefaultExpired time.Duration
|
||||||
|
memCacheCleanupInterval time.Duration
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
memCacheDefaultExpired, _ = time.ParseDuration(env.Get("MEMCACHE_DEFAULT_EXPIRED", "1h"))
|
||||||
|
memCacheCleanupInterval, _ = time.ParseDuration(env.Get("MEMCACHE_CLEANUP_INTERVAL", "10m"))
|
||||||
|
|
||||||
|
if memCacheDefaultExpired < time.Second*5 {
|
||||||
|
memCacheDefaultExpired = time.Second * 5
|
||||||
|
}
|
||||||
|
|
||||||
|
if memCacheCleanupInterval < time.Minute {
|
||||||
|
memCacheCleanupInterval = time.Minute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type MemCache struct {
|
type MemCache struct {
|
||||||
engine *cache.Cache
|
engine *cache.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cache *MemCache) Set(ctx context.Context, key string, value any) {
|
func (cache *MemCache) Try(ctx context.Context, key string, cb LoadFunc) (buf []byte, err error) {
|
||||||
cache.engine.Set(key, value, 0)
|
var (
|
||||||
|
ok bool
|
||||||
|
)
|
||||||
|
if buf, ok = cache.Get(ctx, key); ok {
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
if cb == nil {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
if buf, err = cb(ctx); err == nil {
|
||||||
|
cache.Set(ctx, key, buf)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cache *MemCache) SetEx(ctx context.Context, key string, value any, expire time.Duration) {
|
func (cache *MemCache) Set(ctx context.Context, key string, buf []byte) {
|
||||||
cache.engine.Set(key, value, expire)
|
cache.engine.Set(key, buf, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cache *MemCache) Get(ctx context.Context, key string) (value any, ok bool) {
|
func (cache *MemCache) SetEx(ctx context.Context, key string, buf []byte, expire time.Duration) {
|
||||||
return cache.engine.Get(key)
|
cache.engine.Set(key, buf, expire)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *MemCache) Get(ctx context.Context, key string) (buf []byte, ok bool) {
|
||||||
|
var (
|
||||||
|
val any
|
||||||
|
)
|
||||||
|
if val, ok = cache.engine.Get(key); ok {
|
||||||
|
buf, ok = val.([]byte)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cache *MemCache) Del(ctx context.Context, key string) {
|
func (cache *MemCache) Del(ctx context.Context, key string) {
|
||||||
|
@ -28,6 +70,6 @@ func (cache *MemCache) Del(ctx context.Context, key string) {
|
||||||
|
|
||||||
func NewMemCache() *MemCache {
|
func NewMemCache() *MemCache {
|
||||||
return &MemCache{
|
return &MemCache{
|
||||||
engine: cache.New(time.Hour, time.Minute*10),
|
engine: cache.New(memCacheDefaultExpired, memCacheCleanupInterval),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.nobla.cn/golang/kos/util/env"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,9 +18,22 @@ const (
|
||||||
FG_GREY = 37
|
FG_GREY = 37
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
levelColor = map[int]int{
|
||||||
|
TraceLevel: FG_GREY,
|
||||||
|
DebugLevel: FG_BLUE,
|
||||||
|
InfoLevel: FG_GREEN,
|
||||||
|
WarnLevel: FG_PURPLE,
|
||||||
|
ErrorLevel: FG_RED,
|
||||||
|
FatalLevel: FG_RED,
|
||||||
|
PanicLevel: FG_RED,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
type Console struct {
|
type Console struct {
|
||||||
Level int
|
Level int
|
||||||
EnableColor int
|
DisableColor bool
|
||||||
|
DisableTime bool
|
||||||
prefix string
|
prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,32 +102,29 @@ func (log *Console) Panicf(format string, args ...interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (log *Console) write(level int, s string) {
|
func (log *Console) write(level int, s string) {
|
||||||
|
var ls string
|
||||||
if log.Level > level {
|
if log.Level > level {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lvColor := map[int]int{
|
if log.DisableColor {
|
||||||
TraceLevel: FG_GREY,
|
|
||||||
DebugLevel: FG_BLUE,
|
|
||||||
InfoLevel: FG_GREEN,
|
|
||||||
WarnLevel: FG_PURPLE,
|
|
||||||
ErrorLevel: FG_RED,
|
|
||||||
FatalLevel: FG_RED,
|
|
||||||
PanicLevel: FG_RED,
|
|
||||||
}
|
|
||||||
var ls string
|
|
||||||
if log.EnableColor > 0 {
|
|
||||||
ls = fmt.Sprintf("\033[0m\033[%dm[%s]\033[0m", lvColor[level], getLevelText(level))
|
|
||||||
} else {
|
|
||||||
ls = getLevelText(level)
|
ls = getLevelText(level)
|
||||||
|
} else {
|
||||||
|
ls = fmt.Sprintf("\033[0m\033[%dm[%s]\033[0m", levelColor[level], getLevelText(level))
|
||||||
}
|
}
|
||||||
if log.prefix != "" {
|
if log.prefix != "" {
|
||||||
ls += " [" + log.prefix + "]"
|
ls += " [" + log.prefix + "]"
|
||||||
}
|
}
|
||||||
|
if log.DisableTime {
|
||||||
|
fmt.Println(ls + " " + s)
|
||||||
|
} else {
|
||||||
fmt.Println(time.Now().Format("2006-01-02 15:04:05") + " " + ls + " " + s)
|
fmt.Println(time.Now().Format("2006-01-02 15:04:05") + " " + ls + " " + s)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConsoleLogger() *Console {
|
func NewConsoleLogger() *Console {
|
||||||
return &Console{
|
lg := &Console{}
|
||||||
EnableColor: 1,
|
lg.DisableColor, _ = strconv.ParseBool(env.Get("VOX_DISABLE_LOG_COLOR", "false"))
|
||||||
}
|
lg.DisableTime, _ = strconv.ParseBool(env.Get("VOX_DISABLE_LOG_DATETIME", "false"))
|
||||||
|
return lg
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
package request
|
package request
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -21,6 +24,44 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
UnsafeClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
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 {
|
func (client *Client) stashUri(urlPath string) string {
|
||||||
var (
|
var (
|
||||||
pos int
|
pos int
|
||||||
|
@ -139,7 +180,7 @@ func (client *Client) execute(r *Request) (res *http.Response, err error) {
|
||||||
|
|
||||||
func New() *Client {
|
func New() *Client {
|
||||||
client := &Client{
|
client := &Client{
|
||||||
client: http.DefaultClient,
|
client: DefaultClient,
|
||||||
interceptorRequest: make([]BeforeRequest, 0, 10),
|
interceptorRequest: make([]BeforeRequest, 0, 10),
|
||||||
interceptorResponse: make([]AfterRequest, 0, 10),
|
interceptorResponse: make([]AfterRequest, 0, 10),
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrAccessDenied = 4003 //拒绝访问
|
||||||
|
ErrPermissionDenied = 4004 //没有权限
|
||||||
|
ErrIllegalRequest = 4005 //非法请求
|
||||||
|
ErrInvalidPayload = 4006 //请求数据无效
|
||||||
|
ErrResourceCreate = 4101 //资源创建失败
|
||||||
|
ErrResourceUpdate = 4102 //资源更新失败
|
||||||
|
ErrResourceDelete = 4103 //资源删除失败
|
||||||
|
ErrResourceNotFound = 4104 //资源未找到
|
||||||
|
ErrResourceEmpty = 4105 //资源为空
|
||||||
|
ErrResourceExpired = 4107 //资源已失效
|
||||||
|
ErrResourceUnavailable = 4108 //资源无法使用
|
||||||
|
ErrResourceLocked = 4109 //资源已被锁定
|
||||||
|
ErrServerUnreachable = 4201 //服务不可用
|
||||||
|
ErrTemporaryUnavailable = 4202 //临时性失败
|
||||||
|
ErrFatal = 4204 //致命错误
|
||||||
|
)
|
49
service.go
49
service.go
|
@ -16,12 +16,12 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.nspix.com/golang/kos/entry"
|
"git.nobla.cn/golang/kos/entry"
|
||||||
"git.nspix.com/golang/kos/entry/cli"
|
"git.nobla.cn/golang/kos/entry/cli"
|
||||||
"git.nspix.com/golang/kos/entry/http"
|
"git.nobla.cn/golang/kos/entry/http"
|
||||||
_ "git.nspix.com/golang/kos/pkg/cache"
|
_ "git.nobla.cn/golang/kos/pkg/cache"
|
||||||
"git.nspix.com/golang/kos/pkg/log"
|
"git.nobla.cn/golang/kos/pkg/log"
|
||||||
"git.nspix.com/golang/kos/util/env"
|
"git.nobla.cn/golang/kos/util/env"
|
||||||
"github.com/sourcegraph/conc"
|
"github.com/sourcegraph/conc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -76,14 +76,15 @@ func (app *application) Command() *cli.Server {
|
||||||
return app.command
|
return app.command
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *application) Handle(path string, cb HandleFunc) {
|
func (app *application) Handle(path string, cb HandleFunc, cbs ...HandleOption) {
|
||||||
|
opts := newHandleOptions(cbs...)
|
||||||
if app.http != nil {
|
if app.http != nil {
|
||||||
app.http.Handle(http.MethodPost, path, func(ctx *http.Context) (err error) {
|
app.http.Handle(http.MethodPost, path, func(ctx *http.Context) (err error) {
|
||||||
return cb(ctx)
|
return cb(ctx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if app.command != nil {
|
if app.command != nil {
|
||||||
app.command.Handle(path, "", func(ctx *cli.Context) (err error) {
|
app.command.Handle(path, opts.description, func(ctx *cli.Context) (err error) {
|
||||||
return cb(ctx)
|
return cb(ctx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -93,7 +94,6 @@ func (app *application) httpServe() (err error) {
|
||||||
var (
|
var (
|
||||||
l net.Listener
|
l net.Listener
|
||||||
)
|
)
|
||||||
app.http = http.New(app.ctx)
|
|
||||||
if l, err = app.gateway.Apply(
|
if l, err = app.gateway.Apply(
|
||||||
entry.Feature(http.MethodGet),
|
entry.Feature(http.MethodGet),
|
||||||
entry.Feature(http.MethodHead),
|
entry.Feature(http.MethodHead),
|
||||||
|
@ -140,7 +140,6 @@ func (app *application) commandServe() (err error) {
|
||||||
var (
|
var (
|
||||||
l net.Listener
|
l net.Listener
|
||||||
)
|
)
|
||||||
app.command = cli.New(app.ctx)
|
|
||||||
if l, err = app.gateway.Apply(
|
if l, err = app.gateway.Apply(
|
||||||
cli.Feature,
|
cli.Feature,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
@ -212,6 +211,9 @@ func (app *application) preStart() (err error) {
|
||||||
app.Log().Infof("server starting")
|
app.Log().Infof("server starting")
|
||||||
env.Set(EnvAppName, app.opts.ShortName())
|
env.Set(EnvAppName, app.opts.ShortName())
|
||||||
env.Set(EnvAppVersion, app.opts.Version)
|
env.Set(EnvAppVersion, app.opts.Version)
|
||||||
|
app.http = http.New(app.ctx)
|
||||||
|
app.command = cli.New(app.ctx)
|
||||||
|
if !app.opts.DisableGateway {
|
||||||
addr = net.JoinHostPort(app.opts.Address, strconv.Itoa(app.opts.Port))
|
addr = net.JoinHostPort(app.opts.Address, strconv.Itoa(app.opts.Port))
|
||||||
app.Log().Infof("server listen on: %s", addr)
|
app.Log().Infof("server listen on: %s", addr)
|
||||||
app.gateway = entry.New(addr)
|
app.gateway = entry.New(addr)
|
||||||
|
@ -228,7 +230,7 @@ func (app *application) preStart() (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
app.plugins.Range(func(key, value any) bool {
|
app.plugins.Range(func(key, value any) bool {
|
||||||
if plugin, ok := value.(Plugin); ok {
|
if plugin, ok := value.(Plugin); ok {
|
||||||
if err = plugin.BeforeStart(); err != nil {
|
if err = plugin.BeforeStart(); err != nil {
|
||||||
|
@ -252,11 +254,25 @@ func (app *application) preStart() (err error) {
|
||||||
Uptime: time.Now().Sub(app.uptime).String(),
|
Uptime: time.Now().Sub(app.uptime).String(),
|
||||||
Gateway: app.gateway.State(),
|
Gateway: app.gateway.State(),
|
||||||
})
|
})
|
||||||
})
|
}, WithHandleDescription("Display application state"))
|
||||||
app.Handle("/-/healthy", func(ctx Context) (err error) {
|
app.Handle("/-/healthy", func(ctx Context) (err error) {
|
||||||
return ctx.Success(app.Healthy())
|
return ctx.Success(app.Healthy())
|
||||||
|
}, WithHandleDescription("Display application healthy"))
|
||||||
|
|
||||||
|
if !app.opts.DisableCommand {
|
||||||
|
if app.command != nil {
|
||||||
|
app.command.Handle("/-/debug", "Toggle debug model", func(ctx *cli.Context) (err error) {
|
||||||
|
var (
|
||||||
|
bv bool
|
||||||
|
)
|
||||||
|
if bv, err = strconv.ParseBool(ctx.Argument(0)); err == nil {
|
||||||
|
Debug(bv)
|
||||||
|
}
|
||||||
|
return ctx.Success(Debug())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
app.plugins.Range(func(key, value any) bool {
|
app.plugins.Range(func(key, value any) bool {
|
||||||
if plugin, ok := value.(Plugin); ok {
|
if plugin, ok := value.(Plugin); ok {
|
||||||
if err = plugin.AfterStart(); err != nil {
|
if err = plugin.AfterStart(); err != nil {
|
||||||
|
@ -288,6 +304,7 @@ func (app *application) preStop() (err error) {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
if !app.opts.DisableGateway {
|
||||||
if app.http != nil {
|
if app.http != nil {
|
||||||
if err = app.http.Shutdown(); err != nil {
|
if err = app.http.Shutdown(); err != nil {
|
||||||
app.Log().Warnf("server http shutdown error: %s", err.Error())
|
app.Log().Warnf("server http shutdown error: %s", err.Error())
|
||||||
|
@ -301,6 +318,7 @@ func (app *application) preStop() (err error) {
|
||||||
if err = app.gateway.Stop(); err != nil {
|
if err = app.gateway.Stop(); err != nil {
|
||||||
app.Log().Warnf("server gateway shutdown error: %s", err.Error())
|
app.Log().Warnf("server gateway shutdown error: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
app.plugins.Range(func(key, value any) bool {
|
app.plugins.Range(func(key, value any) bool {
|
||||||
if plugin, ok := value.(Plugin); ok {
|
if plugin, ok := value.(Plugin); ok {
|
||||||
if err = plugin.AfterStop(); err != nil {
|
if err = plugin.AfterStop(); err != nil {
|
||||||
|
@ -344,11 +362,8 @@ func (app *application) Run() (err error) {
|
||||||
return app.preStop()
|
return app.preStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cbs ...Option) *application {
|
func New(cbs ...Option) Application {
|
||||||
opts := NewOptions()
|
opts := NewOptions(cbs...)
|
||||||
for _, cb := range cbs {
|
|
||||||
cb(opts)
|
|
||||||
}
|
|
||||||
app := &application{
|
app := &application{
|
||||||
opts: opts,
|
opts: opts,
|
||||||
uptime: time.Now(),
|
uptime: time.Now(),
|
||||||
|
|
9
types.go
9
types.go
|
@ -2,9 +2,9 @@ package kos
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"git.nspix.com/golang/kos/entry"
|
"git.nobla.cn/golang/kos/entry"
|
||||||
"git.nspix.com/golang/kos/entry/cli"
|
"git.nobla.cn/golang/kos/entry/cli"
|
||||||
"git.nspix.com/golang/kos/entry/http"
|
"git.nobla.cn/golang/kos/entry/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -22,7 +22,8 @@ type (
|
||||||
Info() *Info
|
Info() *Info
|
||||||
Http() *http.Server
|
Http() *http.Server
|
||||||
Command() *cli.Server
|
Command() *cli.Server
|
||||||
Handle(method string, cb HandleFunc)
|
Handle(method string, cb HandleFunc, opts ...HandleOption)
|
||||||
|
Run() (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info application information
|
// Info application information
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
package arrays
|
||||||
|
|
||||||
|
func IndexOf[T comparable](a T, vs []T) int {
|
||||||
|
for i, v := range vs {
|
||||||
|
if v == a {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func Exists[T comparable](a T, vs []T) bool {
|
||||||
|
return IndexOf(a, vs) > -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fill[T any](startIndex int, num uint, value T) map[int]T {
|
||||||
|
m := make(map[int]T)
|
||||||
|
var i uint
|
||||||
|
for i = 0; i < num; i++ {
|
||||||
|
m[startIndex] = value
|
||||||
|
startIndex++
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reverse[T comparable](s []T) []T {
|
||||||
|
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func Merge[T comparable](ss ...[]T) []T {
|
||||||
|
n := 0
|
||||||
|
for _, v := range ss {
|
||||||
|
n += len(v)
|
||||||
|
}
|
||||||
|
s := make([]T, 0, n)
|
||||||
|
for _, v := range ss {
|
||||||
|
s = append(s, v...)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package arrays
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIndexOf(t *testing.T) {
|
||||||
|
type args[T comparable] struct {
|
||||||
|
a T
|
||||||
|
vs []T
|
||||||
|
}
|
||||||
|
type testCase[T comparable] struct {
|
||||||
|
name string
|
||||||
|
args args[T]
|
||||||
|
want int
|
||||||
|
}
|
||||||
|
tests := []testCase[string]{
|
||||||
|
{"exists", args[string]{a: "a", vs: []string{"a", "b"}}, 0},
|
||||||
|
{"not exists", args[string]{a: "a", vs: []string{"c", "b"}}, -1},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := IndexOf(tt.args.a, tt.args.vs); got != tt.want {
|
||||||
|
t.Errorf("IndexOf() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExists(t *testing.T) {
|
||||||
|
type args[T comparable] struct {
|
||||||
|
a T
|
||||||
|
vs []T
|
||||||
|
}
|
||||||
|
type testCase[T comparable] struct {
|
||||||
|
name string
|
||||||
|
args args[T]
|
||||||
|
want bool
|
||||||
|
}
|
||||||
|
tests := []testCase[int]{
|
||||||
|
{"exists", args[int]{a: 1, vs: []int{1, 2}}, true},
|
||||||
|
{"not exists", args[int]{a: 2, vs: []int{3, 4}}, false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := Exists(tt.args.a, tt.args.vs); got != tt.want {
|
||||||
|
t.Errorf("Exists() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReverse(t *testing.T) {
|
||||||
|
type args[T comparable] struct {
|
||||||
|
s []T
|
||||||
|
}
|
||||||
|
type testCase[T comparable] struct {
|
||||||
|
name string
|
||||||
|
args args[T]
|
||||||
|
want []T
|
||||||
|
}
|
||||||
|
tests := []testCase[int]{
|
||||||
|
{"one", args[int]{s: []int{1, 2, 3}}, []int{3, 2, 1}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := Reverse(tt.args.s); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("Reverse() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
//go:build appengine
|
||||||
|
// +build appengine
|
||||||
|
|
||||||
|
package bs
|
||||||
|
|
||||||
|
func BytesToString(b []byte) string {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringToBytes(s string) []byte {
|
||||||
|
return []byte(s)
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
//go:build !appengine
|
||||||
|
// +build !appengine
|
||||||
|
|
||||||
|
package bs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BytesToString converts byte slice to string.
|
||||||
|
func BytesToString(b []byte) string {
|
||||||
|
return *(*string)(unsafe.Pointer(&b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringToBytes converts string to byte slice.
|
||||||
|
func StringToBytes(s string) []byte {
|
||||||
|
return *(*[]byte)(unsafe.Pointer(
|
||||||
|
&struct {
|
||||||
|
string
|
||||||
|
Cap int
|
||||||
|
}{s, len(s)},
|
||||||
|
))
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package crypto
|
package aes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
|
@ -14,11 +14,13 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.nobla.cn/golang/kos/util/env"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
httpClient = http.Client{
|
httpClient = http.Client{
|
||||||
Timeout: time.Second * 15,
|
Timeout: time.Second * 30,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
|
@ -29,7 +31,7 @@ var (
|
||||||
KeepAlive: 30 * time.Second,
|
KeepAlive: 30 * time.Second,
|
||||||
}).DialContext,
|
}).DialContext,
|
||||||
ForceAttemptHTTP2: false,
|
ForceAttemptHTTP2: false,
|
||||||
MaxIdleConns: 10,
|
MaxIdleConns: 48,
|
||||||
IdleConnTimeout: 30 * time.Second,
|
IdleConnTimeout: 30 * time.Second,
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
@ -37,6 +39,15 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
httpDefaultTimeout := env.Get("HTTP_CLIENT_TIMEOUT", "30s")
|
||||||
|
if httpDefaultTimeout != "" {
|
||||||
|
if duration, err := time.ParseDuration(httpDefaultTimeout); err == nil {
|
||||||
|
httpClient.Timeout = duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func encode(data any) (r io.Reader, contentType string, err error) {
|
func encode(data any) (r io.Reader, contentType string, err error) {
|
||||||
var (
|
var (
|
||||||
buf []byte
|
buf []byte
|
||||||
|
@ -57,6 +68,7 @@ func encode(data any) (r io.Reader, contentType string, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
func Get(ctx context.Context, urlString string, cbs ...Option) (res *http.Response, err error) {
|
||||||
var (
|
var (
|
||||||
uri *url.URL
|
uri *url.URL
|
||||||
|
@ -76,7 +88,7 @@ func Get(ctx context.Context, urlString string, cbs ...Option) (res *http.Respon
|
||||||
}
|
}
|
||||||
uri.RawQuery = qs.Encode()
|
uri.RawQuery = qs.Encode()
|
||||||
}
|
}
|
||||||
if req, err = http.NewRequest(http.MethodGet, uri.String(), nil); err != nil {
|
if req, err = http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if opts.Header != nil {
|
if opts.Header != nil {
|
||||||
|
@ -84,9 +96,10 @@ func Get(ctx context.Context, urlString string, cbs ...Option) (res *http.Respon
|
||||||
req.Header.Set(k, v)
|
req.Header.Set(k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return do(ctx, req, opts)
|
return do(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) {
|
func Post(ctx context.Context, urlString string, cbs ...Option) (res *http.Response, err error) {
|
||||||
var (
|
var (
|
||||||
uri *url.URL
|
uri *url.URL
|
||||||
|
@ -113,7 +126,7 @@ func Post(ctx context.Context, urlString string, cbs ...Option) (res *http.Respo
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if req, err = http.NewRequest(http.MethodPost, uri.String(), reader); err != nil {
|
if req, err = http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), reader); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if opts.Header != nil {
|
if opts.Header != nil {
|
||||||
|
@ -124,17 +137,17 @@ func Post(ctx context.Context, urlString string, cbs ...Option) (res *http.Respo
|
||||||
if contentType != "" {
|
if contentType != "" {
|
||||||
req.Header.Set("Content-Type", contentType)
|
req.Header.Set("Content-Type", contentType)
|
||||||
}
|
}
|
||||||
return do(ctx, req, opts)
|
return do(req, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Request(ctx context.Context, urlString string, response any, cbs ...Option) (err error) {
|
// Request is a generic HTTP request function that can handle GET, POST, PUT, DELETE, etc.
|
||||||
|
func Request(ctx context.Context, urlString string, result any, cbs ...Option) (err error) {
|
||||||
var (
|
var (
|
||||||
buf []byte
|
contentType string
|
||||||
|
reader io.Reader
|
||||||
uri *url.URL
|
uri *url.URL
|
||||||
res *http.Response
|
res *http.Response
|
||||||
req *http.Request
|
req *http.Request
|
||||||
contentType string
|
|
||||||
reader io.Reader
|
|
||||||
)
|
)
|
||||||
opts := newOptions()
|
opts := newOptions()
|
||||||
for _, cb := range cbs {
|
for _, cb := range cbs {
|
||||||
|
@ -155,7 +168,7 @@ func Request(ctx context.Context, urlString string, response any, cbs ...Option)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if req, err = http.NewRequest(opts.Method, uri.String(), reader); err != nil {
|
if req, err = http.NewRequestWithContext(ctx, opts.Method, uri.String(), reader); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if opts.Header != nil {
|
if opts.Header != nil {
|
||||||
|
@ -166,41 +179,41 @@ func Request(ctx context.Context, urlString string, response any, cbs ...Option)
|
||||||
if contentType != "" {
|
if contentType != "" {
|
||||||
req.Header.Set("Content-Type", contentType)
|
req.Header.Set("Content-Type", contentType)
|
||||||
}
|
}
|
||||||
if res, err = do(ctx, req, opts); err != nil {
|
if res, err = do(req, opts); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = res.Body.Close()
|
_ = res.Body.Close()
|
||||||
}()
|
}()
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
if buf, err = io.ReadAll(res.Body); err == nil && len(buf) > 0 {
|
err = fmt.Errorf("ubexpected HTTP status code: %d", res.StatusCode)
|
||||||
err = fmt.Errorf("remote server response %s(%d): %s", res.Status, res.StatusCode, string(buf))
|
|
||||||
} else {
|
|
||||||
err = fmt.Errorf("remote server response %d: %s", res.StatusCode, res.Status)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
//don't care response
|
||||||
|
if result == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
contentType = strings.ToLower(res.Header.Get("Content-Type"))
|
contentType = strings.ToLower(res.Header.Get("Content-Type"))
|
||||||
extName := path.Ext(req.URL.String())
|
extName := path.Ext(req.URL.String())
|
||||||
if strings.Contains(contentType, JSON) || extName == ".json" {
|
if strings.Contains(contentType, JSON) || extName == ".json" {
|
||||||
err = json.NewDecoder(res.Body).Decode(response)
|
err = json.NewDecoder(res.Body).Decode(result)
|
||||||
} else if strings.Contains(contentType, XML) || extName == ".xml" {
|
} else if strings.Contains(contentType, XML) || extName == ".xml" {
|
||||||
err = xml.NewDecoder(res.Body).Decode(response)
|
err = xml.NewDecoder(res.Body).Decode(result)
|
||||||
} else {
|
} else {
|
||||||
err = fmt.Errorf("unsupported content type: %s", contentType)
|
err = fmt.Errorf("unsupported content type: %s", contentType)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func Do(ctx context.Context, req *http.Request, cbs ...Option) (res *http.Response, err error) {
|
func Do(req *http.Request, cbs ...Option) (res *http.Response, err error) {
|
||||||
opts := newOptions()
|
opts := newOptions()
|
||||||
for _, cb := range cbs {
|
for _, cb := range cbs {
|
||||||
cb(opts)
|
cb(opts)
|
||||||
}
|
}
|
||||||
return do(ctx, req, opts)
|
return do(req, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func do(ctx context.Context, req *http.Request, opts *Options) (res *http.Response, err error) {
|
func do(req *http.Request, opts *Options) (res *http.Response, err error) {
|
||||||
if opts.Human {
|
if opts.Human {
|
||||||
if req.Header.Get("User-Agent") == "" {
|
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")
|
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")
|
||||||
|
@ -208,9 +221,6 @@ func do(ctx context.Context, req *http.Request, opts *Options) (res *http.Respon
|
||||||
if req.Header.Get("Referer") == "" {
|
if req.Header.Get("Referer") == "" {
|
||||||
req.Header.Set("Referer", req.URL.String())
|
req.Header.Set("Referer", req.URL.String())
|
||||||
}
|
}
|
||||||
if req.Header.Get("Accept") == "" {
|
|
||||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
|
|
||||||
}
|
}
|
||||||
}
|
return httpClient.Do(req)
|
||||||
return httpClient.Do(req.WithContext(ctx))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package fs
|
package fs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -11,22 +10,26 @@ func IsDir(filename string) (bool, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
fm := fd.Mode()
|
return fd.Mode().IsDir(), nil
|
||||||
return fm.IsDir(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DirectoryOrCreate checking directory, is not exists will create
|
// IsFile Tells whether the filename is a file
|
||||||
func DirectoryOrCreate(dirname string) error {
|
func IsFile(filename string) (bool, error) {
|
||||||
if fi, err := os.Stat(dirname); err != nil {
|
fd, err := os.Stat(filename)
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if err != nil {
|
||||||
return os.MkdirAll(dirname, 0755)
|
return false, err
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
return !fd.Mode().IsDir(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mkdir checking directory, is not exists will create
|
||||||
|
func Mkdir(dirname string, perm os.FileMode) error {
|
||||||
|
if fi, err := os.Stat(dirname); err != nil {
|
||||||
|
return os.MkdirAll(dirname, perm)
|
||||||
} else {
|
} else {
|
||||||
if fi.IsDir() {
|
if fi.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return errors.New("file not directory")
|
return os.ErrPermission
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
package fs
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
package humanize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"git.nobla.cn/golang/kos/util/bs"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Nanosecond Duration = 1
|
||||||
|
Microsecond = 1000 * Nanosecond
|
||||||
|
Millisecond = 1000 * Microsecond
|
||||||
|
Second = 1000 * Millisecond
|
||||||
|
Minute = 60 * Second
|
||||||
|
Hour = 60 * Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
type Duration int64
|
||||||
|
|
||||||
|
// fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the
|
||||||
|
// tail of buf, omitting trailing zeros. It omits the decimal
|
||||||
|
// point too when the fraction is 0. It returns the index where the
|
||||||
|
// output bytes begin and the value v/10**prec.
|
||||||
|
func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) {
|
||||||
|
// Omit trailing zeros up to and including decimal point.
|
||||||
|
w := len(buf)
|
||||||
|
print := false
|
||||||
|
for i := 0; i < prec; i++ {
|
||||||
|
digit := v % 10
|
||||||
|
print = print || digit != 0
|
||||||
|
if print {
|
||||||
|
w--
|
||||||
|
buf[w] = byte(digit) + '0'
|
||||||
|
}
|
||||||
|
v /= 10
|
||||||
|
}
|
||||||
|
if print {
|
||||||
|
w--
|
||||||
|
buf[w] = '.'
|
||||||
|
}
|
||||||
|
return w, v
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmtInt formats v into the tail of buf.
|
||||||
|
// It returns the index where the output begins.
|
||||||
|
func fmtInt(buf []byte, v uint64) int {
|
||||||
|
w := len(buf)
|
||||||
|
if v == 0 {
|
||||||
|
w--
|
||||||
|
buf[w] = '0'
|
||||||
|
} else {
|
||||||
|
for v > 0 {
|
||||||
|
w--
|
||||||
|
buf[w] = byte(v%10) + '0'
|
||||||
|
v /= 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Duration) String() string {
|
||||||
|
// Largest time is 2540400h10m10.000000000s
|
||||||
|
var buf [32]byte
|
||||||
|
w := len(buf)
|
||||||
|
|
||||||
|
u := uint64(d)
|
||||||
|
neg := d < 0
|
||||||
|
if neg {
|
||||||
|
u = -u
|
||||||
|
}
|
||||||
|
|
||||||
|
if u < uint64(time.Second) {
|
||||||
|
// Special case: if duration is smaller than a second,
|
||||||
|
// use smaller units, like 1.2ms
|
||||||
|
var prec int
|
||||||
|
w--
|
||||||
|
buf[w] = 's'
|
||||||
|
w--
|
||||||
|
switch {
|
||||||
|
case u == 0:
|
||||||
|
return "0s"
|
||||||
|
case u < uint64(time.Microsecond):
|
||||||
|
// print nanoseconds
|
||||||
|
prec = 0
|
||||||
|
buf[w] = 'n'
|
||||||
|
case u < uint64(time.Millisecond):
|
||||||
|
// print microseconds
|
||||||
|
prec = 3
|
||||||
|
// U+00B5 'µ' micro sign == 0xC2 0xB5
|
||||||
|
w-- // Need room for two bytes.
|
||||||
|
copy(buf[w:], "µ")
|
||||||
|
default:
|
||||||
|
// print milliseconds
|
||||||
|
prec = 6
|
||||||
|
buf[w] = 'm'
|
||||||
|
}
|
||||||
|
w, u = fmtFrac(buf[:w], u, prec)
|
||||||
|
w = fmtInt(buf[:w], u)
|
||||||
|
} else {
|
||||||
|
w--
|
||||||
|
buf[w] = 's'
|
||||||
|
|
||||||
|
w, u = fmtFrac(buf[:w], u, 9)
|
||||||
|
|
||||||
|
// u is now integer seconds
|
||||||
|
w = fmtInt(buf[:w], u%60)
|
||||||
|
u /= 60
|
||||||
|
|
||||||
|
// u is now integer minutes
|
||||||
|
if u > 0 {
|
||||||
|
w--
|
||||||
|
buf[w] = 'm'
|
||||||
|
w = fmtInt(buf[:w], u%60)
|
||||||
|
u /= 60
|
||||||
|
|
||||||
|
// u is now integer hours
|
||||||
|
// Stop at hours because days can be different lengths.
|
||||||
|
if u > 0 {
|
||||||
|
w--
|
||||||
|
buf[w] = 'h'
|
||||||
|
w = fmtInt(buf[:w], u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if neg {
|
||||||
|
w--
|
||||||
|
buf[w] = '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(buf[w:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Duration) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
var n time.Duration
|
||||||
|
b = bytes.TrimFunc(b, func(r rune) bool {
|
||||||
|
if r == '"' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if n, err = time.ParseDuration(bs.BytesToString(b)); err == nil {
|
||||||
|
*d = Duration(n)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||||
|
return bs.StringToBytes(`"` + d.String() + `"`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Duration) Duration() time.Duration {
|
||||||
|
return time.Duration(d)
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
package humanize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
renderFloatPrecisionMultipliers = [...]float64{
|
||||||
|
1,
|
||||||
|
10,
|
||||||
|
100,
|
||||||
|
1000,
|
||||||
|
10000,
|
||||||
|
100000,
|
||||||
|
1000000,
|
||||||
|
10000000,
|
||||||
|
100000000,
|
||||||
|
1000000000,
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFloatPrecisionRounders = [...]float64{
|
||||||
|
0.5,
|
||||||
|
0.05,
|
||||||
|
0.005,
|
||||||
|
0.0005,
|
||||||
|
0.00005,
|
||||||
|
0.000005,
|
||||||
|
0.0000005,
|
||||||
|
0.00000005,
|
||||||
|
0.000000005,
|
||||||
|
0.0000000005,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// FormatFloat produces a formatted number as string based on the following user-specified criteria:
|
||||||
|
// * thousands separator
|
||||||
|
// * decimal separator
|
||||||
|
// * decimal precision
|
||||||
|
//
|
||||||
|
// Usage: s := RenderFloat(format, n)
|
||||||
|
// The format parameter tells how to render the number n.
|
||||||
|
//
|
||||||
|
// See examples: http://play.golang.org/p/LXc1Ddm1lJ
|
||||||
|
//
|
||||||
|
// Examples of format strings, given n = 12345.6789:
|
||||||
|
// "#,###.##" => "12,345.67"
|
||||||
|
// "#,###." => "12,345"
|
||||||
|
// "#,###" => "12345,678"
|
||||||
|
// "#\u202F###,##" => "12 345,68"
|
||||||
|
// "#.###,###### => 12.345,678900
|
||||||
|
// "" (aka default format) => 12,345.67
|
||||||
|
//
|
||||||
|
// The highest precision allowed is 9 digits after the decimal symbol.
|
||||||
|
// There is also a version for integer number, FormatInteger(),
|
||||||
|
// which is convenient for calls within template.
|
||||||
|
func FormatFloat(format string, n float64) string {
|
||||||
|
// Special cases:
|
||||||
|
// NaN = "NaN"
|
||||||
|
// +Inf = "+Infinity"
|
||||||
|
// -Inf = "-Infinity"
|
||||||
|
if math.IsNaN(n) {
|
||||||
|
return "NaN"
|
||||||
|
}
|
||||||
|
if n > math.MaxFloat64 {
|
||||||
|
return "Infinity"
|
||||||
|
}
|
||||||
|
if n < (0.0 - math.MaxFloat64) {
|
||||||
|
return "-Infinity"
|
||||||
|
}
|
||||||
|
|
||||||
|
// default format
|
||||||
|
precision := 2
|
||||||
|
decimalStr := "."
|
||||||
|
thousandStr := ","
|
||||||
|
positiveStr := ""
|
||||||
|
negativeStr := "-"
|
||||||
|
|
||||||
|
if len(format) > 0 {
|
||||||
|
format := []rune(format)
|
||||||
|
|
||||||
|
// If there is an explicit format directive,
|
||||||
|
// then default values are these:
|
||||||
|
precision = 9
|
||||||
|
thousandStr = ""
|
||||||
|
|
||||||
|
// collect indices of meaningful formatting directives
|
||||||
|
formatIndx := []int{}
|
||||||
|
for i, char := range format {
|
||||||
|
if char != '#' && char != '0' {
|
||||||
|
formatIndx = append(formatIndx, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(formatIndx) > 0 {
|
||||||
|
// Directive at index 0:
|
||||||
|
// Must be a '+'
|
||||||
|
// Raise an error if not the case
|
||||||
|
// index: 0123456789
|
||||||
|
// +0.000,000
|
||||||
|
// +000,000.0
|
||||||
|
// +0000.00
|
||||||
|
// +0000
|
||||||
|
if formatIndx[0] == 0 {
|
||||||
|
if format[formatIndx[0]] != '+' {
|
||||||
|
panic("RenderFloat(): invalid positive sign directive")
|
||||||
|
}
|
||||||
|
positiveStr = "+"
|
||||||
|
formatIndx = formatIndx[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two directives:
|
||||||
|
// First is thousands separator
|
||||||
|
// Raise an error if not followed by 3-digit
|
||||||
|
// 0123456789
|
||||||
|
// 0.000,000
|
||||||
|
// 000,000.00
|
||||||
|
if len(formatIndx) == 2 {
|
||||||
|
if (formatIndx[1] - formatIndx[0]) != 4 {
|
||||||
|
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
|
||||||
|
}
|
||||||
|
thousandStr = string(format[formatIndx[0]])
|
||||||
|
formatIndx = formatIndx[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// One directive:
|
||||||
|
// Directive is decimal separator
|
||||||
|
// The number of digit-specifier following the separator indicates wanted precision
|
||||||
|
// 0123456789
|
||||||
|
// 0.00
|
||||||
|
// 000,0000
|
||||||
|
if len(formatIndx) == 1 {
|
||||||
|
decimalStr = string(format[formatIndx[0]])
|
||||||
|
precision = len(format) - formatIndx[0] - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate sign part
|
||||||
|
var signStr string
|
||||||
|
if n >= 0.000000001 {
|
||||||
|
signStr = positiveStr
|
||||||
|
} else if n <= -0.000000001 {
|
||||||
|
signStr = negativeStr
|
||||||
|
n = -n
|
||||||
|
} else {
|
||||||
|
signStr = ""
|
||||||
|
n = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// split number into integer and fractional parts
|
||||||
|
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
|
||||||
|
|
||||||
|
// generate integer part string
|
||||||
|
intStr := strconv.FormatInt(int64(intf), 10)
|
||||||
|
|
||||||
|
// add thousand separator if required
|
||||||
|
if len(thousandStr) > 0 {
|
||||||
|
for i := len(intStr); i > 3; {
|
||||||
|
i -= 3
|
||||||
|
intStr = intStr[:i] + thousandStr + intStr[i:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no fractional part, we can leave now
|
||||||
|
if precision == 0 {
|
||||||
|
return signStr + intStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate fractional part
|
||||||
|
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
|
||||||
|
// may need padding
|
||||||
|
if len(fracStr) < precision {
|
||||||
|
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
|
||||||
|
}
|
||||||
|
|
||||||
|
return signStr + intStr + decimalStr + fracStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatInteger produces a formatted number as string.
|
||||||
|
// See FormatFloat.
|
||||||
|
func FormatInteger(format string, n int) string {
|
||||||
|
return FormatFloat(format, float64(n))
|
||||||
|
}
|
|
@ -0,0 +1,173 @@
|
||||||
|
package humanize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"git.nobla.cn/golang/kos/util/bs"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Size uint64
|
||||||
|
|
||||||
|
// IEC Sizes.
|
||||||
|
// kibis of bits
|
||||||
|
const (
|
||||||
|
Byte = 1 << (iota * 10)
|
||||||
|
KiByte
|
||||||
|
MiByte
|
||||||
|
GiByte
|
||||||
|
TiByte
|
||||||
|
PiByte
|
||||||
|
EiByte
|
||||||
|
)
|
||||||
|
|
||||||
|
// SI Sizes.
|
||||||
|
const (
|
||||||
|
IByte = 1
|
||||||
|
KByte = IByte * 1000
|
||||||
|
MByte = KByte * 1000
|
||||||
|
GByte = MByte * 1000
|
||||||
|
TByte = GByte * 1000
|
||||||
|
PByte = TByte * 1000
|
||||||
|
EByte = PByte * 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
var bytesSizeTable = map[string]uint64{
|
||||||
|
"b": Byte,
|
||||||
|
"kib": KiByte,
|
||||||
|
"kb": KByte,
|
||||||
|
"mib": MiByte,
|
||||||
|
"mb": MByte,
|
||||||
|
"gib": GiByte,
|
||||||
|
"gb": GByte,
|
||||||
|
"tib": TiByte,
|
||||||
|
"tb": TByte,
|
||||||
|
"pib": PiByte,
|
||||||
|
"pb": PByte,
|
||||||
|
"eib": EiByte,
|
||||||
|
"eb": EByte,
|
||||||
|
// Without suffix
|
||||||
|
"": Byte,
|
||||||
|
"ki": KiByte,
|
||||||
|
"k": KByte,
|
||||||
|
"mi": MiByte,
|
||||||
|
"m": MByte,
|
||||||
|
"gi": GiByte,
|
||||||
|
"g": GByte,
|
||||||
|
"ti": TiByte,
|
||||||
|
"t": TByte,
|
||||||
|
"pi": PiByte,
|
||||||
|
"p": PByte,
|
||||||
|
"ei": EiByte,
|
||||||
|
"e": EByte,
|
||||||
|
}
|
||||||
|
|
||||||
|
func logn(n, b float64) float64 {
|
||||||
|
return math.Log(n) / math.Log(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||||
|
if s < 10 {
|
||||||
|
return fmt.Sprintf("%d B", s)
|
||||||
|
}
|
||||||
|
e := math.Floor(logn(float64(s), base))
|
||||||
|
suffix := sizes[int(e)]
|
||||||
|
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||||
|
f := "%.0f %s"
|
||||||
|
if val < 10 {
|
||||||
|
f = "%.1f %s"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(f, val, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes produces a human readable representation of an SI size.
|
||||||
|
//
|
||||||
|
// See also: ParseBytes.
|
||||||
|
//
|
||||||
|
// Bytes(82854982) -> 83 MB
|
||||||
|
func Bytes(s uint64) string {
|
||||||
|
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
||||||
|
return humanateBytes(s, 1000, sizes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IBytes produces a human readable representation of an IEC size.
|
||||||
|
//
|
||||||
|
// See also: ParseBytes.
|
||||||
|
//
|
||||||
|
// IBytes(82854982) -> 79 MiB
|
||||||
|
func IBytes(s uint64) string {
|
||||||
|
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||||
|
return humanateBytes(s, 1024, sizes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseBytes parses a string representation of bytes into the number
|
||||||
|
// of bytes it represents.
|
||||||
|
//
|
||||||
|
// See Also: Bytes, IBytes.
|
||||||
|
//
|
||||||
|
// ParseBytes("42 MB") -> 42000000, nil
|
||||||
|
// ParseBytes("42 mib") -> 44040192, nil
|
||||||
|
func ParseBytes(s string) (uint64, error) {
|
||||||
|
lastDigit := 0
|
||||||
|
hasComma := false
|
||||||
|
for _, r := range s {
|
||||||
|
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if r == ',' {
|
||||||
|
hasComma = true
|
||||||
|
}
|
||||||
|
lastDigit++
|
||||||
|
}
|
||||||
|
|
||||||
|
num := s[:lastDigit]
|
||||||
|
if hasComma {
|
||||||
|
num = strings.Replace(num, ",", "", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := strconv.ParseFloat(num, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||||
|
if m, ok := bytesSizeTable[extra]; ok {
|
||||||
|
f *= float64(m)
|
||||||
|
if f >= math.MaxUint64 {
|
||||||
|
return 0, fmt.Errorf("too large: %v", s)
|
||||||
|
}
|
||||||
|
return uint64(f), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Size) UnmarshalJSON(buf []byte) error {
|
||||||
|
var (
|
||||||
|
n uint64
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
buf = bytes.TrimFunc(buf, func(r rune) bool {
|
||||||
|
if r == '"' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if n, err = ParseBytes(bs.BytesToString(buf)); err == nil {
|
||||||
|
*b = Size(n)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Size) MarshalJSON() ([]byte, error) {
|
||||||
|
s := `"` + IBytes(uint64(b)) + `"`
|
||||||
|
return bs.StringToBytes(s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Size) String() string {
|
||||||
|
return IBytes(uint64(b))
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
package humanize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"git.nobla.cn/golang/kos/util/bs"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Day = 24 * time.Hour
|
||||||
|
Week = 7 * Day
|
||||||
|
Month = 30 * Day
|
||||||
|
Year = 12 * Month
|
||||||
|
LongTime = 37 * Year
|
||||||
|
)
|
||||||
|
|
||||||
|
// A RelTimeMagnitude struct contains a relative time point at which
|
||||||
|
// the relative format of time will switch to a new format string. A
|
||||||
|
// slice of these in ascending order by their "D" field is passed to
|
||||||
|
// CustomRelTime to format durations.
|
||||||
|
//
|
||||||
|
// The Format field is a string that may contain a "%s" which will be
|
||||||
|
// replaced with the appropriate signed label (e.g. "ago" or "from
|
||||||
|
// now") and a "%d" that will be replaced by the quantity.
|
||||||
|
//
|
||||||
|
// The DivBy field is the amount of time the time difference must be
|
||||||
|
// divided by in order to display correctly.
|
||||||
|
//
|
||||||
|
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
|
||||||
|
// DivBy should be time.Minute so whatever the duration is will be
|
||||||
|
// expressed in minutes.
|
||||||
|
type RelTimeMagnitude struct {
|
||||||
|
D time.Duration
|
||||||
|
Format string
|
||||||
|
DivBy time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultMagnitudes = []RelTimeMagnitude{
|
||||||
|
{time.Second, "now", time.Second},
|
||||||
|
{2 * time.Second, "1 second %s", 1},
|
||||||
|
{time.Minute, "%d seconds %s", time.Second},
|
||||||
|
{2 * time.Minute, "1 minute %s", 1},
|
||||||
|
{time.Hour, "%d minutes %s", time.Minute},
|
||||||
|
{2 * time.Hour, "1 hour %s", 1},
|
||||||
|
{Day, "%d hours %s", time.Hour},
|
||||||
|
{2 * Day, "1 day %s", 1},
|
||||||
|
{Week, "%d days %s", Day},
|
||||||
|
{2 * Week, "1 week %s", 1},
|
||||||
|
{Month, "%d weeks %s", Week},
|
||||||
|
{2 * Month, "1 month %s", 1},
|
||||||
|
{Year, "%d months %s", Month},
|
||||||
|
{18 * Month, "1 year %s", 1},
|
||||||
|
{2 * Year, "2 years %s", 1},
|
||||||
|
{LongTime, "%d years %s", Year},
|
||||||
|
{math.MaxInt64, "a long while %s", 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
// RelTime formats a time into a relative string.
|
||||||
|
//
|
||||||
|
// It takes two times and two labels. In addition to the generic time
|
||||||
|
// delta string (e.g. 5 minutes), the labels are used applied so that
|
||||||
|
// the label corresponding to the smaller time is applied.
|
||||||
|
//
|
||||||
|
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
|
||||||
|
func RelTime(a, b time.Time, albl, blbl string) string {
|
||||||
|
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomRelTime formats a time into a relative string.
|
||||||
|
//
|
||||||
|
// It takes two times two labels and a table of relative time formats.
|
||||||
|
// In addition to the generic time delta string (e.g. 5 minutes), the
|
||||||
|
// labels are used applied so that the label corresponding to the
|
||||||
|
// smaller time is applied.
|
||||||
|
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
|
||||||
|
lbl := albl
|
||||||
|
diff := b.Sub(a)
|
||||||
|
|
||||||
|
if a.After(b) {
|
||||||
|
lbl = blbl
|
||||||
|
diff = a.Sub(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
n := sort.Search(len(magnitudes), func(i int) bool {
|
||||||
|
return magnitudes[i].D > diff
|
||||||
|
})
|
||||||
|
|
||||||
|
if n >= len(magnitudes) {
|
||||||
|
n = len(magnitudes) - 1
|
||||||
|
}
|
||||||
|
mag := magnitudes[n]
|
||||||
|
args := []interface{}{}
|
||||||
|
escaped := false
|
||||||
|
for _, ch := range mag.Format {
|
||||||
|
if escaped {
|
||||||
|
switch ch {
|
||||||
|
case 's':
|
||||||
|
args = append(args, lbl)
|
||||||
|
case 'd':
|
||||||
|
args = append(args, diff/mag.DivBy)
|
||||||
|
}
|
||||||
|
escaped = false
|
||||||
|
} else {
|
||||||
|
escaped = ch == '%'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(mag.Format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Time struct {
|
||||||
|
tm time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func Now() Time {
|
||||||
|
return Time{tm: time.Now()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WrapTime(t time.Time) Time {
|
||||||
|
return Time{tm: t}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) Add(d Duration) Time {
|
||||||
|
t.tm = t.tm.Add(d.Duration())
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) AddDuration(d time.Duration) Time {
|
||||||
|
t.tm = t.tm.Add(d)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) After(u Time) bool {
|
||||||
|
return t.tm.After(u.tm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) AfterTime(u time.Time) bool {
|
||||||
|
return t.tm.After(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) Sub(u Time) Duration {
|
||||||
|
return Duration(t.tm.Sub(u.tm))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) SubTime(u time.Time) Duration {
|
||||||
|
return Duration(t.tm.Sub(u))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) Time() time.Time {
|
||||||
|
return t.tm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) String() string {
|
||||||
|
return t.tm.Format(time.DateTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) MarshalJSON() ([]byte, error) {
|
||||||
|
s := `"` + t.tm.Format(time.DateTime) + `"`
|
||||||
|
return bs.StringToBytes(s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) Ago() string {
|
||||||
|
return RelTime(t.tm, time.Now(), "ago", "from now")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Time) UnmarshalJSON(buf []byte) (err error) {
|
||||||
|
buf = bytes.TrimFunc(buf, func(r rune) bool {
|
||||||
|
if r == '"' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
t.tm, err = time.ParseInLocation(time.DateTime, bs.BytesToString(buf), time.Local)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package humanize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type test struct {
|
||||||
|
Time Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNow(t *testing.T) {
|
||||||
|
tm := Now().Add(-1 * Hour * 223)
|
||||||
|
t.Log(tm.Ago())
|
||||||
|
ts := &test{Time: Now()}
|
||||||
|
buf, err := json.Marshal(ts)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
t.Log(string(buf))
|
||||||
|
vv := &test{}
|
||||||
|
if err = json.Unmarshal(buf, vv); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
t.Log(vv.Time)
|
||||||
|
}
|
|
@ -1,23 +1,28 @@
|
||||||
package reflect
|
package reflect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
allowTags = []string{"json", "yaml", "xml", "name"}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrValueAssociated = errors.New("value cannot be associated")
|
||||||
|
)
|
||||||
|
|
||||||
func findField(v reflect.Value, field string) reflect.Value {
|
func findField(v reflect.Value, field string) reflect.Value {
|
||||||
var (
|
var (
|
||||||
pos int
|
pos int
|
||||||
tagValue string
|
tagValue string
|
||||||
refValue reflect.Value
|
|
||||||
refType reflect.Type
|
refType reflect.Type
|
||||||
fieldType reflect.StructField
|
fieldType reflect.StructField
|
||||||
allowTags = []string{"json", "yaml", "xml"}
|
|
||||||
)
|
)
|
||||||
refValue = v.FieldByName(field)
|
|
||||||
if !refValue.IsValid() {
|
|
||||||
refType = v.Type()
|
refType = v.Type()
|
||||||
for i := 0; i < refType.NumField(); i++ {
|
for i := 0; i < refType.NumField(); i++ {
|
||||||
fieldType = refType.Field(i)
|
fieldType = refType.Field(i)
|
||||||
|
@ -26,7 +31,7 @@ func findField(v reflect.Value, field string) reflect.Value {
|
||||||
if tagValue == "" {
|
if tagValue == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if pos = strings.Index(tagValue, ","); pos != -1 {
|
if pos = strings.IndexByte(tagValue, ','); pos != -1 {
|
||||||
tagValue = tagValue[:pos]
|
tagValue = tagValue[:pos]
|
||||||
}
|
}
|
||||||
if tagValue == field {
|
if tagValue == field {
|
||||||
|
@ -34,8 +39,11 @@ func findField(v reflect.Value, field string) reflect.Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return v.FieldByName(field)
|
||||||
return refValue
|
}
|
||||||
|
|
||||||
|
func Assign(variable reflect.Value, value interface{}) (err error) {
|
||||||
|
return safeAssignment(variable, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func safeAssignment(variable reflect.Value, value interface{}) (err error) {
|
func safeAssignment(variable reflect.Value, value interface{}) (err error) {
|
||||||
|
@ -52,8 +60,45 @@ func safeAssignment(variable reflect.Value, value interface{}) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch kind {
|
switch kind {
|
||||||
|
case reflect.Bool:
|
||||||
|
switch rv.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
variable.SetBool(rv.Bool())
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
if rv.Int() != 0 {
|
||||||
|
variable.SetBool(true)
|
||||||
|
} else {
|
||||||
|
variable.SetBool(false)
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
if rv.Uint() != 0 {
|
||||||
|
variable.SetBool(true)
|
||||||
|
} else {
|
||||||
|
variable.SetBool(false)
|
||||||
|
}
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
if rv.Float() != 0 {
|
||||||
|
variable.SetBool(true)
|
||||||
|
} else {
|
||||||
|
variable.SetBool(false)
|
||||||
|
}
|
||||||
|
case reflect.String:
|
||||||
|
var tv bool
|
||||||
|
tv, err = strconv.ParseBool(rv.String())
|
||||||
|
if err == nil {
|
||||||
|
variable.SetBool(tv)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("boolean value can not assign %s", rv.Kind())
|
||||||
|
}
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
switch rv.Kind() {
|
switch rv.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
if rv.Bool() {
|
||||||
|
variable.SetInt(1)
|
||||||
|
} else {
|
||||||
|
variable.SetInt(0)
|
||||||
|
}
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
variable.SetInt(rv.Int())
|
variable.SetInt(rv.Int())
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
@ -65,10 +110,16 @@ func safeAssignment(variable reflect.Value, value interface{}) (err error) {
|
||||||
variable.SetInt(n)
|
variable.SetInt(n)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("unsupported kind %s", rv.Kind())
|
err = fmt.Errorf("integer value can not assign %s", rv.Kind())
|
||||||
}
|
}
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
switch rv.Kind() {
|
switch rv.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
if rv.Bool() {
|
||||||
|
variable.SetUint(1)
|
||||||
|
} else {
|
||||||
|
variable.SetUint(0)
|
||||||
|
}
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
variable.SetUint(uint64(rv.Int()))
|
variable.SetUint(uint64(rv.Int()))
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
@ -80,10 +131,16 @@ func safeAssignment(variable reflect.Value, value interface{}) (err error) {
|
||||||
variable.SetUint(un)
|
variable.SetUint(un)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("unsupported kind %s", rv.Kind())
|
err = fmt.Errorf("unsigned integer value can not assign %s", rv.Kind())
|
||||||
}
|
}
|
||||||
case reflect.Float32, reflect.Float64:
|
case reflect.Float32, reflect.Float64:
|
||||||
switch rv.Kind() {
|
switch rv.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
if rv.Bool() {
|
||||||
|
variable.SetFloat(1)
|
||||||
|
} else {
|
||||||
|
variable.SetFloat(0)
|
||||||
|
}
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
variable.SetFloat(float64(rv.Int()))
|
variable.SetFloat(float64(rv.Int()))
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
@ -95,10 +152,16 @@ func safeAssignment(variable reflect.Value, value interface{}) (err error) {
|
||||||
variable.SetFloat(fn)
|
variable.SetFloat(fn)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("unsupported kind %s", rv.Kind())
|
err = fmt.Errorf("decimal value can not assign %s", rv.Kind())
|
||||||
}
|
}
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
switch rv.Kind() {
|
switch rv.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
if rv.Bool() {
|
||||||
|
variable.SetString("true")
|
||||||
|
} else {
|
||||||
|
variable.SetString("false")
|
||||||
|
}
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
variable.SetString(strconv.FormatInt(rv.Int(), 10))
|
variable.SetString(strconv.FormatInt(rv.Int(), 10))
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
@ -110,6 +173,8 @@ func safeAssignment(variable reflect.Value, value interface{}) (err error) {
|
||||||
default:
|
default:
|
||||||
variable.SetString(fmt.Sprint(value))
|
variable.SetString(fmt.Sprint(value))
|
||||||
}
|
}
|
||||||
|
case reflect.Interface:
|
||||||
|
variable.Set(rv)
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("unsupported kind %s", kind)
|
err = fmt.Errorf("unsupported kind %s", kind)
|
||||||
}
|
}
|
||||||
|
@ -139,21 +204,136 @@ func Set(hacky interface{}, field string, value interface{}) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch fieldKind {
|
switch fieldKind {
|
||||||
|
case reflect.Struct:
|
||||||
|
if rv.Kind() != reflect.Map {
|
||||||
|
return ErrValueAssociated
|
||||||
|
}
|
||||||
|
keys := rv.MapKeys()
|
||||||
|
subVal := reflect.New(refField.Type())
|
||||||
|
for _, key := range keys {
|
||||||
|
pv := rv.MapIndex(key)
|
||||||
|
if key.Kind() == reflect.String {
|
||||||
|
if err = Set(subVal.Interface(), key.String(), pv.Interface()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refField.Set(subVal.Elem())
|
||||||
|
case reflect.Ptr:
|
||||||
|
elemType := refField.Type()
|
||||||
|
if elemType.Elem().Kind() != reflect.Struct {
|
||||||
|
return ErrValueAssociated
|
||||||
|
} else {
|
||||||
|
if rv.Kind() != reflect.Map {
|
||||||
|
return ErrValueAssociated
|
||||||
|
}
|
||||||
|
keys := rv.MapKeys()
|
||||||
|
subVal := reflect.New(elemType.Elem())
|
||||||
|
for _, key := range keys {
|
||||||
|
pv := rv.MapIndex(key)
|
||||||
|
if key.Kind() == reflect.String {
|
||||||
|
if err = Set(subVal.Interface(), key.String(), pv.Interface()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refField.Set(subVal)
|
||||||
|
}
|
||||||
|
case reflect.Map:
|
||||||
|
if rv.Kind() != reflect.Map {
|
||||||
|
return ErrValueAssociated
|
||||||
|
}
|
||||||
|
targetValue := reflect.MakeMap(refField.Type())
|
||||||
|
keys := rv.MapKeys()
|
||||||
|
for _, key := range keys {
|
||||||
|
pv := rv.MapIndex(key)
|
||||||
|
kVal := reflect.New(refField.Type().Key())
|
||||||
|
eVal := reflect.New(refField.Type().Elem())
|
||||||
|
if err = safeAssignment(kVal.Elem(), key.Interface()); err != nil {
|
||||||
|
return ErrValueAssociated
|
||||||
|
}
|
||||||
|
if refField.Type().Elem().Kind() == reflect.Struct {
|
||||||
|
if pv.Elem().Kind() != reflect.Map {
|
||||||
|
return ErrValueAssociated
|
||||||
|
}
|
||||||
|
subKeys := pv.Elem().MapKeys()
|
||||||
|
for _, subKey := range subKeys {
|
||||||
|
subVal := pv.Elem().MapIndex(subKey)
|
||||||
|
if subKey.Kind() == reflect.String {
|
||||||
|
if err = Set(eVal.Interface(), subKey.String(), subVal.Interface()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
targetValue.SetMapIndex(kVal.Elem(), eVal.Elem())
|
||||||
|
} else {
|
||||||
|
if err = safeAssignment(eVal.Elem(), pv.Interface()); err != nil {
|
||||||
|
return ErrValueAssociated
|
||||||
|
}
|
||||||
|
targetValue.SetMapIndex(kVal.Elem(), eVal.Elem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refField.Set(targetValue)
|
||||||
case reflect.Array, reflect.Slice:
|
case reflect.Array, reflect.Slice:
|
||||||
|
n = 0
|
||||||
innerType := refField.Type().Elem()
|
innerType := refField.Type().Elem()
|
||||||
if rv.Kind() == reflect.Array || rv.Kind() == reflect.Slice {
|
if rv.Kind() == reflect.Array || rv.Kind() == reflect.Slice {
|
||||||
|
if innerType.Kind() == reflect.Struct {
|
||||||
sliceVar := reflect.MakeSlice(refField.Type(), rv.Len(), rv.Len())
|
sliceVar := reflect.MakeSlice(refField.Type(), rv.Len(), rv.Len())
|
||||||
n = 0
|
|
||||||
for i := 0; i < rv.Len(); i++ {
|
for i := 0; i < rv.Len(); i++ {
|
||||||
srcVal := rv.Index(i)
|
srcVal := rv.Index(i)
|
||||||
dstVal := reflect.New(innerType).Elem()
|
if srcVal.Kind() != reflect.Map {
|
||||||
if err = safeAssignment(dstVal, srcVal); err == nil {
|
return ErrValueAssociated
|
||||||
|
}
|
||||||
|
dstVal := reflect.New(innerType)
|
||||||
|
keys := srcVal.MapKeys()
|
||||||
|
for _, key := range keys {
|
||||||
|
kv := srcVal.MapIndex(key)
|
||||||
|
if key.Kind() == reflect.String {
|
||||||
|
if err = Set(dstVal.Interface(), key.String(), kv.Interface()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sliceVar.Index(n).Set(dstVal.Elem())
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
refField.Set(sliceVar.Slice(0, n))
|
||||||
|
} else if innerType.Kind() == reflect.Ptr {
|
||||||
|
sliceVar := reflect.MakeSlice(refField.Type(), rv.Len(), rv.Len())
|
||||||
|
for i := 0; i < rv.Len(); i++ {
|
||||||
|
srcVal := rv.Index(i)
|
||||||
|
if srcVal.Kind() != reflect.Map {
|
||||||
|
return ErrValueAssociated
|
||||||
|
}
|
||||||
|
dstVal := reflect.New(innerType.Elem())
|
||||||
|
keys := srcVal.MapKeys()
|
||||||
|
for _, key := range keys {
|
||||||
|
kv := srcVal.MapIndex(key)
|
||||||
|
if key.Kind() == reflect.String {
|
||||||
|
if err = Set(dstVal.Interface(), key.String(), kv.Interface()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
sliceVar.Index(n).Set(dstVal)
|
sliceVar.Index(n).Set(dstVal)
|
||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
|
refField.Set(sliceVar.Slice(0, n))
|
||||||
|
} else {
|
||||||
|
sliceVar := reflect.MakeSlice(refField.Type(), rv.Len(), rv.Len())
|
||||||
|
for i := 0; i < rv.Len(); i++ {
|
||||||
|
srcVal := rv.Index(i)
|
||||||
|
dstVal := reflect.New(innerType).Elem()
|
||||||
|
if err = safeAssignment(dstVal, srcVal.Interface()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sliceVar.Index(n).Set(dstVal)
|
||||||
|
n++
|
||||||
}
|
}
|
||||||
refField.Set(sliceVar.Slice(0, n))
|
refField.Set(sliceVar.Slice(0, n))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err = safeAssignment(refField, value)
|
err = safeAssignment(refField, value)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package reflect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSet(t *testing.T) {
|
||||||
|
type hack struct {
|
||||||
|
Duration time.Duration
|
||||||
|
Enable bool
|
||||||
|
}
|
||||||
|
h := &hack{}
|
||||||
|
Set(h, "Duration", "1111111111111111")
|
||||||
|
Set(h, "Enable", "T")
|
||||||
|
t.Log(h.Duration)
|
||||||
|
t.Log(h.Enable)
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package reflection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.nobla.cn/golang/kos/util/reflect"
|
||||||
|
reflectpkg "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Setter[T string | int | int64 | float64 | any](hacky any, variables map[string]T) (err error) {
|
||||||
|
for k, v := range variables {
|
||||||
|
if err = Set(hacky, k, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Assign(s reflectpkg.Value, v any) error {
|
||||||
|
return reflect.Assign(s, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Set(hacky any, field string, value any) (err error) {
|
||||||
|
return reflect.Set(hacky, field, value)
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package reflection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Fakeb struct {
|
||||||
|
In int `json:"in"`
|
||||||
|
BS map[string]string `json:"bs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ab struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type fake struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
Usage []Fakeb `json:"usage"`
|
||||||
|
XX Fakeb `json:"xx"`
|
||||||
|
AX *Fakeb `json:"ax"`
|
||||||
|
SS []string `json:"ss"`
|
||||||
|
DS []int `json:"ds"`
|
||||||
|
Ms map[string]int `json:"ms"`
|
||||||
|
AB map[string]Ab `json:"ab"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetter(t *testing.T) {
|
||||||
|
dst := &fake{}
|
||||||
|
vs := map[string]any{
|
||||||
|
"name": "xxx",
|
||||||
|
}
|
||||||
|
vvs := []map[string]any{
|
||||||
|
{
|
||||||
|
"in": 15,
|
||||||
|
"bs": map[string]any{
|
||||||
|
"aa": "vv",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ms := map[string]any{
|
||||||
|
"name": "aa",
|
||||||
|
"age": "5",
|
||||||
|
"usage": vvs,
|
||||||
|
"xx": map[string]any{"in": 45},
|
||||||
|
"ax": map[string]any{"in": 55},
|
||||||
|
"ss": []string{"11", "ss"},
|
||||||
|
"ds": []int{55, 55, 34},
|
||||||
|
"ms": map[string]any{"aa": "23"},
|
||||||
|
"ab": map[string]any{
|
||||||
|
"xx": vs,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := Setter(dst, ms)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if dst.Age != 5 {
|
||||||
|
t.Errorf("setter failed")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%+v", dst)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,19 +2,15 @@ package sys
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HomeDir return user home directory
|
// HomeDir return user home directory
|
||||||
func HomeDir() string {
|
func HomeDir() string {
|
||||||
if runtime.GOOS == "windows" {
|
if dirname, err := os.UserHomeDir(); err == nil {
|
||||||
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
|
return dirname
|
||||||
}
|
}
|
||||||
if h := os.Getenv("HOME"); h != "" {
|
return os.TempDir()
|
||||||
return h
|
|
||||||
}
|
|
||||||
return "/"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HiddenFile get hidden file prefix
|
// HiddenFile get hidden file prefix
|
||||||
|
@ -29,20 +25,12 @@ func HiddenFile(name string) string {
|
||||||
|
|
||||||
// CacheDir return user cache directory
|
// CacheDir return user cache directory
|
||||||
func CacheDir() string {
|
func CacheDir() string {
|
||||||
switch runtime.GOOS {
|
if dirname, err := os.UserCacheDir(); err == nil {
|
||||||
case "darwin":
|
return dirname
|
||||||
return filepath.Join(HomeDir(), "Library", "Caches")
|
|
||||||
case "windows":
|
|
||||||
for _, ev := range []string{"APPDATA", "CSIDL_APPDATA", "TEMP", "TMP"} {
|
|
||||||
if v := os.Getenv(ev); v != "" {
|
|
||||||
return v
|
|
||||||
}
|
}
|
||||||
}
|
return os.TempDir()
|
||||||
// Worst case:
|
}
|
||||||
return HomeDir()
|
|
||||||
}
|
func TempFile() (*os.File, error) {
|
||||||
if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
|
return os.CreateTemp(os.TempDir(), "kos_*")
|
||||||
return xdg
|
|
||||||
}
|
|
||||||
return filepath.Join(HomeDir(), ".cache")
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue