Compare commits

..

43 Commits

Author SHA1 Message Date
Yavolte b4ebbba30c add error types define 2025-03-31 11:18:33 +08:00
Yavolte ac427e9134 optimize code for fetch components 2025-03-31 11:11:42 +08:00
fancl 77ee2d8712 add debug model 2025-03-05 13:51:53 +08:00
fancl 129a9e768d change package names 2024-11-12 17:47:28 +08:00
fancl 9dbae23345 fix serialize bugs 2024-11-12 17:36:44 +08:00
fancl be8b56b897 fix json encode 2024-07-19 17:37:34 +08:00
fancl b28dbdb8c1 fix cli shutdown panic 2024-06-14 11:32:20 +08:00
fancl 7fff2f8a1c fix command line exclude column 2024-04-29 15:17:23 +08:00
fancl bef2e35e41 fix bugs 2024-04-29 14:58:03 +08:00
fancl 57c134b6ba 优化最小服务 2024-04-29 10:52:19 +08:00
fancl 281b44f637 添加before操作 2024-04-15 11:49:42 +08:00
fancl 0cd890fbb0 add user clear 2024-02-27 09:46:46 +08:00
fancl 634261a27c add boolean supported 2024-02-22 17:53:31 +08:00
fancl a3835e2ead 修复context问题和添加变量支持 2024-01-19 10:04:37 +08:00
fancl 5dac511cae 添加超时功能处理 2024-01-18 17:13:27 +08:00
fancl 972eb004d2 优化cli命令行的问题 2024-01-18 17:11:44 +08:00
fancl 83afa05fa3 add assign method 2023-12-06 17:09:47 +08:00
fancl df640f65fa add instance 2023-10-23 15:47:37 +08:00
fancl eb4c287da0 add name and version 2023-10-23 15:44:44 +08:00
fancl 809c45c301 添加地址端口 2023-10-23 15:43:46 +08:00
fancl b91e28a512 update cache protocol 2023-09-20 11:05:08 +08:00
fancl 183f940f65 add context user supported 2023-09-20 10:16:05 +08:00
fancl 2db8293160 add interface supported 2023-09-12 16:50:45 +08:00
fancl 9cfa81115f add array struct and map supported 2023-09-12 15:42:28 +08:00
fancl fbb08c6eb4 add struct 2023-09-12 14:14:09 +08:00
fancl c42b2317d6 add document root 2023-08-25 09:53:45 +08:00
fancl 4b346afaec 新增文档根目录处理逻辑 2023-08-25 09:51:03 +08:00
fancl 6693cfe68f 新增修改http状态码 2023-08-24 11:02:47 +08:00
fancl b73084c1b5 rename fetch 2023-08-23 17:12:08 +08:00
fancl ffac331e3d include package 2023-08-23 14:13:45 +08:00
fancl 2877f751dc fix bugs 2023-08-22 16:11:34 +08:00
fancl 2c5c83578c fix bug 2023-08-16 09:57:42 +08:00
fancl d24f7efb0b fix real ip 2023-08-15 17:42:32 +08:00
fancl 4199b81b5f add humanize package 2023-08-07 14:57:16 +08:00
fancl 4114e5fcb0 fix bug 2023-07-07 09:53:37 +08:00
fancl a995627a52 update package 2023-06-30 14:16:16 +08:00
fancl 302b90842b add embed resource cache 2023-06-19 16:13:34 +08:00
sugar 9c47b20864 add 32bit supported 2023-06-11 11:19:01 +08:00
fancl b1e60de34a change request reader 2023-06-06 18:14:00 +08:00
fancl f3b532ec67 add request lib 2023-06-06 10:59:13 +08:00
fancl dc88ceb73d add gateway support direct connect 2023-05-31 11:14:15 +08:00
fancl 8d716e837d add real ip take 2023-05-06 16:15:04 +08:00
fancl cdc1af1b37 fix package 2023-04-26 16:08:54 +08:00
52 changed files with 2768 additions and 295 deletions

View File

@ -4,8 +4,13 @@ import (
"context"
"embed"
"flag"
"git.nspix.com/golang/kos"
"git.nspix.com/golang/kos/pkg/log"
httpkg "net/http"
"time"
"git.nobla.cn/golang/kos/entry/cli"
"git.nobla.cn/golang/kos/entry/http"
"git.nobla.cn/golang/kos"
)
//go:embed web
@ -14,20 +19,50 @@ var webDir embed.FS
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) {
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
}
func (s *subServer) Stop() (err error) {
log.Debugf("stopxxx")
return
}
func main() {
flag.Parse()
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{}),
)
svr.Run()

View File

@ -6,7 +6,7 @@
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">
<title>Document</title>
<link rel="stylesheet" href="css/index.css">
<link rel="stylesheet" href="/css/index.css">
</head>
<body>
<h1>Hello</h1>

View File

@ -5,6 +5,7 @@ const (
EnvAppVersion = "VOX_VERSION"
EnvAppPort = "VOX_PORT"
EnvAppAddress = "VOX_ADDRESS"
EnvAppDebug = "VOX_DEBUG"
)
const (

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"git.nobla.cn/golang/kos/util/env"
"github.com/peterh/liner"
"io"
"math"
@ -141,7 +142,7 @@ func (client *Client) completer(str string) (ss []string) {
)
ss = make([]string, 0)
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
}
select {
@ -166,10 +167,10 @@ func (client *Client) Execute(s string) (err error) {
}()
go client.ioLoop(client.conn)
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
}
client.waitResponse(seq, time.Second*30)
client.waitResponse(seq, client.Timeout)
return
}
@ -185,7 +186,7 @@ func (client *Client) Shell() (err error) {
defer func() {
_ = 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
}
go client.ioLoop(client.conn)
@ -216,7 +217,7 @@ func (client *Client) Shell() (err error) {
continue
}
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
}
client.liner.AppendHistory(line)
@ -240,14 +241,22 @@ func (client *Client) Close() (err error) {
}
func NewClient(ctx context.Context, addr string) *Client {
var (
err error
timeout time.Duration
)
if ctx == nil {
ctx = context.Background()
}
duration := env.Get("VOX_TIMEOUT", "30s")
if timeout, err = time.ParseDuration(duration); err != nil {
timeout = time.Second * 30
}
return &Client{
ctx: ctx,
address: addr,
name: filepath.Base(os.Args[0]),
Timeout: time.Second * 30,
Timeout: timeout,
liner: liner.NewLiner(),
readyChan: make(chan struct{}, 1),
exitChan: make(chan struct{}),

View File

@ -1,25 +1,32 @@
package cli
import (
"context"
"fmt"
"io"
"math"
"sync"
)
type Context struct {
Id int64
seq uint16
wc io.WriteCloser
params map[string]string
args []string
Id int64
seq uint16
ctx context.Context
wc io.WriteCloser
params map[string]string
locker sync.RWMutex
variables map[string]any
args []string
}
func (ctx *Context) reset(id int64, wc io.WriteCloser) {
ctx.Id = id
ctx.wc = wc
ctx.seq = 0
ctx.ctx = context.Background()
ctx.args = make([]string, 0)
ctx.params = make(map[string]string)
ctx.variables = make(map[string]any)
}
func (ctx *Context) setArgs(args []string) {
@ -34,6 +41,14 @@ func (ctx *Context) Bind(v any) (err error) {
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 {
if index >= len(ctx.args) || index < 0 {
return ""
@ -48,6 +63,22 @@ func (ctx *Context) Param(s string) string {
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) {
return ctx.send(responsePayload{Type: PacketTypeCommand, Data: v})
}
@ -93,12 +124,12 @@ __END:
chunkSize := math.MaxInt16 - 1
n := len(buf) / chunkSize
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
}
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
}

View File

@ -27,6 +27,7 @@ type (
Seq uint16 `json:"seq"`
Data []byte `json:"data"`
Error string `json:"error"`
Timeout int64 `json:"timeout"`
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 {
return
}
if err = binary.Read(r, binary.LittleEndian, &frame.Timeout); err != nil {
return
}
if err = binary.Read(r, binary.LittleEndian, &frame.Timestamp); err != nil {
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 {
return
}
if err = binary.Write(w, binary.LittleEndian, frame.Timeout); err != nil {
return
}
if err = binary.Write(w, binary.LittleEndian, frame.Timestamp); err != nil {
return
}
@ -142,13 +149,14 @@ func writeFrame(w io.Writer, frame *Frame) (err error) {
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{
Feature: Feature,
Type: t,
Flag: f,
Seq: seq,
Data: data,
Timeout: int64(timeout),
Timestamp: time.Now().Unix(),
}
}

View File

@ -97,6 +97,9 @@ func (r *Router) Handle(path string, command Command) {
name = path
path = ""
}
if name == "-" {
name = "app"
}
children := r.getChildren(name)
if children == nil {
children = newRouter(name)

View File

@ -4,7 +4,8 @@ import (
"bytes"
"encoding/json"
"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"
"reflect"
"strconv"
@ -170,7 +171,7 @@ func serializeArray(val []any) (buf []byte, err error) {
if rv.Kind() == reflect.Array || rv.Kind() == reflect.Slice {
row := make([]any, 0, rv.Len())
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())
} else {
goto __END
@ -184,6 +185,7 @@ func serializeArray(val []any) (buf []byte, err error) {
}
if isStructElement {
vs = make([][]any, 0, len(val))
indexes := make([]int, 0)
for i, v := range val {
rv := reflect.Indirect(reflect.ValueOf(v))
if rv.Kind() == reflect.Struct {
@ -191,16 +193,26 @@ func serializeArray(val []any) (buf []byte, err error) {
row := make([]any, 0, rv.Type().NumField())
for j := 0; j < rv.Type().NumField(); j++ {
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)
} else {
if columnName == "-" {
continue
}
}
if !rv.Type().Field(j).IsExported() {
continue
}
indexes = append(indexes, j)
row = append(row, columnName)
}
vs = append(vs, row)
}
row := make([]any, 0, rv.Type().NumField())
for j := 0; j < rv.Type().NumField(); j++ {
row = append(row, rv.Field(j).Interface())
if arrays.Exists(j, indexes) {
row = append(row, rv.Field(j).Interface())
}
}
vs = append(vs, row)
} else {

View File

@ -4,8 +4,7 @@ import (
"context"
"errors"
"fmt"
"git.nspix.com/golang/kos/util/env"
"github.com/sourcegraph/conc"
"math"
"net"
"path"
"runtime"
@ -13,6 +12,9 @@ import (
"sync"
"sync/atomic"
"time"
"git.nobla.cn/golang/kos/util/env"
"github.com/sourcegraph/conc"
)
var (
@ -20,13 +22,15 @@ var (
)
type Server struct {
ctx context.Context
sequence int64
ctxMap sync.Map
waitGroup conc.WaitGroup
middleware []Middleware
router *Router
l net.Listener
ctx context.Context
sequenceLocker sync.Mutex
sequence int64
ctxMap sync.Map
waitGroup conc.WaitGroup
middleware []Middleware
router *Router
l net.Listener
exitFlag int32
}
func (svr *Server) applyContext() *Context {
@ -42,9 +46,8 @@ func (svr *Server) releaseContext(ctx *Context) {
ctxPool.Put(ctx)
}
func (svr *Server) handle(ctx *Context, frame *Frame) {
func (svr *Server) execute(ctx *Context, frame *Frame) (err error) {
var (
err error
params map[string]string
tokens []string
args []string
@ -52,6 +55,15 @@ func (svr *Server) handle(ctx *Context, frame *Frame) {
)
cmd := string(frame.Data)
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 errors.Is(err, ErrNotFound) {
err = ctx.Error(errNotFound, fmt.Sprintf("Command %s not found", cmd))
@ -73,6 +85,17 @@ func (svr *Server) handle(ctx *Context, frame *Frame) {
ctx.setParam(params)
err = r.command.Handle(ctx)
}
return
}
func (svr *Server) nextSequence() int64 {
svr.sequenceLocker.Lock()
defer svr.sequenceLocker.Unlock()
if svr.sequence >= math.MaxInt64 {
svr.sequence = 1
}
svr.sequence++
return svr.sequence
}
func (svr *Server) process(conn net.Conn) {
@ -82,7 +105,7 @@ func (svr *Server) process(conn net.Conn) {
frame *Frame
)
ctx = svr.applyContext()
ctx.reset(atomic.AddInt64(&svr.sequence, 1), conn)
ctx.reset(svr.nextSequence(), conn)
svr.ctxMap.Store(ctx.Id, ctx)
defer func() {
_ = conn.Close()
@ -118,7 +141,9 @@ func (svr *Server) process(conn net.Conn) {
break
}
case PacketTypeCommand:
svr.handle(ctx, frame)
if err = svr.execute(ctx, frame); err != nil {
break
}
default:
break
}
@ -177,11 +202,17 @@ func (svr *Server) Serve(l net.Listener) (err error) {
return ctx.Success(svr.router.String())
})
svr.serve()
atomic.StoreInt32(&svr.exitFlag, 0)
return
}
func (svr *Server) Shutdown() (err error) {
err = svr.l.Close()
if !atomic.CompareAndSwapInt32(&svr.exitFlag, 0, 1) {
return
}
if svr.l != nil {
err = svr.l.Close()
}
svr.ctxMap.Range(func(key, value any) bool {
if ctx, ok := value.(*Context); ok {
err = ctx.Close()

View File

@ -23,20 +23,20 @@ func (c *Conn) Read(b []byte) (n int, err error) {
}
n, err = c.conn.Read(b[m:])
n += m
atomic.AddInt64(&c.state.Traffic.In, int64(n))
c.state.IncTrafficIn(int64(n))
return
}
func (c *Conn) Write(b []byte) (n int, err error) {
n, err = c.conn.Write(b)
atomic.AddInt64(&c.state.Traffic.Out, int64(n))
c.state.IncTrafficOut(int64(n))
return
}
func (c *Conn) Close() error {
if atomic.CompareAndSwapInt32(&c.exitFlag, 0, 1) {
atomic.AddInt32(&c.state.Concurrency, -1)
atomic.AddInt64(&c.state.Request.Processed, 1)
c.state.IncRequestProcessed(1)
return c.conn.Close()
}
return nil

View File

@ -4,11 +4,12 @@ import (
"bytes"
"context"
"errors"
"github.com/sourcegraph/conc"
"io"
"net"
"sync/atomic"
"time"
"github.com/sourcegraph/conc"
)
const (
@ -37,22 +38,23 @@ type (
state *State
waitGroup conc.WaitGroup
listeners []*listenerEntity
direct *Listener
exitFlag int32
}
)
func (gw *Gateway) handle(conn net.Conn) {
var (
n int
err error
successed int32
feature = make([]byte, minFeatureLength)
n int
err error
success int32
feature = make([]byte, minFeatureLength)
)
atomic.AddInt32(&gw.state.Concurrency, 1)
defer func() {
if atomic.LoadInt32(&successed) != 1 {
if atomic.LoadInt32(&success) != 1 {
atomic.AddInt32(&gw.state.Concurrency, -1)
atomic.AddInt64(&gw.state.Request.Discarded, 1)
gw.state.IncRequestDiscarded(1)
_ = conn.Close()
}
}()
@ -70,7 +72,7 @@ func (gw *Gateway) handle(conn net.Conn) {
}
for _, l := range gw.listeners {
if bytes.Compare(feature[:n], l.feature[:n]) == 0 {
atomic.StoreInt32(&successed, 1)
atomic.StoreInt32(&success, 1)
l.listener.Receive(wrapConn(conn, gw.state, feature[:n]))
return
}
@ -86,11 +88,16 @@ func (gw *Gateway) accept() {
if conn, err := gw.l.Accept(); err != nil {
break
} else {
select {
case gw.ch <- conn:
atomic.AddInt64(&gw.state.Request.Total, 1)
case <-gw.ctx.Done():
return
//give direct listener
if gw.direct != nil {
gw.direct.Receive(conn)
} else {
select {
case gw.ch <- conn:
gw.state.IncRequest(1)
case <-gw.ctx.Done():
return
}
}
}
}
@ -113,6 +120,12 @@ func (gw *Gateway) worker() {
}
}
func (gw *Gateway) Direct(l net.Listener) {
if ls, ok := l.(*Listener); ok {
gw.direct = ls
}
}
func (gw *Gateway) Bind(feature Feature, listener net.Listener) (err error) {
var (
ok bool
@ -165,7 +178,9 @@ func (gw *Gateway) Start(ctx context.Context) (err error) {
if gw.l, err = net.Listen("tcp", gw.address); err != nil {
return
}
gw.waitGroup.Go(gw.worker)
for i := 0; i < 2; i++ {
gw.waitGroup.Go(gw.worker)
}
gw.waitGroup.Go(gw.accept)
return
}

View File

@ -3,6 +3,7 @@ package http
import (
"context"
"encoding/json"
"net"
"net/http"
"os"
"path"
@ -13,19 +14,65 @@ var (
defaultBinder = &DefaultBinder{}
)
var (
realIPHeaders = []string{
"Cf-Connecting-Ip",
"True-Client-IP",
"X-Forwarded-For",
"X-Real-Ip",
}
)
type Context struct {
ctx context.Context
req *http.Request
res http.ResponseWriter
params map[string]string
user *Userinfo
statusCode int
}
func (ctx *Context) reset(req *http.Request, res http.ResponseWriter, ps map[string]string) {
ctx.statusCode = http.StatusOK
ctx.user = nil
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 {
var (
s string
pos int
ipaddr string
)
for _, h := range realIPHeaders {
if ipaddr = ctx.Request().Header.Get(h); ipaddr != "" {
goto __end
}
}
ipaddr, _, _ = net.SplitHostPort(ctx.Request().RemoteAddr)
__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
}
ipaddr = ipaddr[pos+1:]
} else {
break
}
}
return strings.TrimSpace(ipaddr)
}
func (ctx *Context) Request() *http.Request {
return ctx.req
}
@ -46,23 +93,36 @@ func (ctx *Context) Bind(v any) (err error) {
}
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)
}
func (ctx *Context) Param(k string) string {
var (
ok bool
v string
s string
)
if v, ok = ctx.params[k]; ok {
return v
if s, ok = ctx.params[k]; ok {
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")
encoder := json.NewEncoder(ctx.Response())
encoder.SetEscapeHTML(false)
if strings.HasPrefix(ctx.Request().Header.Get("User-Agent"), "curl") {
encoder.SetIndent("", "\t")
}
@ -70,7 +130,7 @@ func (ctx *Context) send(res responsePayload) (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) {
@ -78,12 +138,12 @@ func (ctx *Context) Status(code int) {
}
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) {
if code != http.StatusFound && code != http.StatusMovedPermanently {
code = http.StatusMovedPermanently
code = http.StatusFound
}
http.Redirect(ctx.Response(), ctx.Request(), url, code)
}
@ -92,6 +152,44 @@ func (ctx *Context) SetCookie(cookie *http.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) {
var (
fi os.FileInfo

View File

@ -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)
}

View File

@ -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 //致命错误
)

149
entry/http/file.go 100644
View File

@ -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,
}
}

View File

@ -3,12 +3,14 @@ package http
import (
"context"
"embed"
"git.nspix.com/golang/kos/entry/http/router"
"git.nobla.cn/golang/kos/entry/http/router"
"net"
"net/http"
"path"
"strings"
"sync"
"sync/atomic"
"time"
)
var (
@ -16,10 +18,16 @@ var (
)
type Server struct {
ctx context.Context
serve *http.Server
router *router.Router
middleware []Middleware
ctx context.Context
serve *http.Server
router *router.Router
middleware []Middleware
uptime time.Time
enableDocumentRoot bool
fileSystem http.FileSystem
beforeRequests []HandleFunc
anyRequests map[string]http.Handler
exitFlag int32
}
func (svr *Server) applyContext() *Context {
@ -38,14 +46,23 @@ func (svr *Server) releaseContext(ctx *Context) {
func (svr *Server) wrapHandle(cb HandleFunc, middleware ...Middleware) router.Handle {
return func(writer http.ResponseWriter, request *http.Request, params router.Params) {
ctx := svr.applyContext()
ps := make(map[string]string, 4)
defer func() {
svr.releaseContext(ctx)
ps = make(map[string]string, 0)
}()
ps := make(map[string]string)
for _, v := range params {
ps[v.Key] = v.Value
}
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-- {
cb = svr.middleware[i](cb)
}
@ -58,10 +75,21 @@ 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) {
svr.middleware = append(svr.middleware, middleware...)
}
func (svr *Server) Any(prefix string, handle http.Handler) {
if !strings.HasPrefix(prefix, "/") {
prefix = "/" + prefix
}
svr.anyRequests[prefix] = handle
}
func (svr *Server) Handle(method string, path string, cb HandleFunc, middleware ...Middleware) {
if method == "" {
method = http.MethodPost
@ -81,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) {
routePath := prefix
if !strings.HasSuffix(routePath, "/*filepath") {
@ -107,7 +144,7 @@ func (svr *Server) Embed(prefix string, root string, embedFs embed.FS) {
filename = "/" + 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
})
}
@ -149,6 +186,24 @@ func (svr *Server) handleRequest(res http.ResponseWriter, req *http.Request) {
}
func (svr *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
var (
err error
file http.File
)
for prefix, handle := range svr.anyRequests {
if strings.HasPrefix(request.URL.Path, prefix) {
handle.ServeHTTP(writer, request)
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 {
case http.MethodOptions:
svr.handleOption(writer, request)
@ -163,10 +218,14 @@ func (svr *Server) Serve(l net.Listener) (err error) {
}
svr.router.NotFound = NotFound{}
svr.router.MethodNotAllowed = NotAllowed{}
atomic.StoreInt32(&svr.exitFlag, 0)
return svr.serve.Serve(l)
}
func (svr *Server) Shutdown() (err error) {
if !atomic.CompareAndSwapInt32(&svr.exitFlag, 0, 1) {
return
}
if svr.serve != nil {
err = svr.serve.Shutdown(svr.ctx)
}
@ -175,9 +234,12 @@ func (svr *Server) Shutdown() (err error) {
func New(ctx context.Context) *Server {
svr := &Server{
ctx: ctx,
router: router.New(),
middleware: make([]Middleware, 0, 10),
ctx: ctx,
uptime: time.Now(),
router: router.New(),
beforeRequests: make([]HandleFunc, 0, 10),
anyRequests: make(map[string]http.Handler),
middleware: make([]Middleware, 0, 10),
}
return svr
}

30
entry/http/user.go 100644
View File

@ -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)
}
}

View File

@ -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")
}
}

View File

@ -1,6 +1,9 @@
package entry
import "sync"
type State struct {
mutex sync.Mutex
Accepting int32 `json:"accepting"` //是否正在接收连接
Processing int32 `json:"processing"` //是否正在处理连接
Concurrency int32 `json:"concurrency"`
@ -14,3 +17,33 @@ type State struct {
Out int64 `json:"out"` //出网流量
} `json:"traffic"`
}
func (s *State) IncRequest(n int64) {
s.mutex.Lock()
s.Request.Total += n
s.mutex.Unlock()
}
func (s *State) IncRequestProcessed(n int64) {
s.mutex.Lock()
s.Request.Processed += n
s.mutex.Unlock()
}
func (s *State) IncRequestDiscarded(n int64) {
s.mutex.Lock()
s.Request.Discarded += n
s.mutex.Unlock()
}
func (s *State) IncTrafficIn(n int64) {
s.mutex.Lock()
s.Traffic.In += n
s.mutex.Unlock()
}
func (s *State) IncTrafficOut(n int64) {
s.mutex.Lock()
s.Traffic.Out += n
s.mutex.Unlock()
}

2
go.mod
View File

@ -1,4 +1,4 @@
module git.nspix.com/golang/kos
module git.nobla.cn/golang/kos
go 1.20

View File

@ -1,43 +1,83 @@
package kos
import (
"git.nspix.com/golang/kos/entry/cli"
"git.nspix.com/golang/kos/entry/http"
"git.nobla.cn/golang/kos/entry/cli"
"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"
)
var (
once sync.Once
std *application
app Application
)
func initApplication(cbs ...Option) {
func initialization(cbs ...Option) {
once.Do(func() {
std = New(cbs...)
app = New(cbs...)
})
}
func Init(cbs ...Option) *application {
initApplication(cbs...)
return std
func Init(cbs ...Option) Application {
initialization(cbs...)
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 {
initApplication()
return std.Info()
initialization()
return app.Info()
}
func Http() *http.Server {
initApplication()
return std.Http()
initialization()
return app.Http()
}
func Command() *cli.Server {
initApplication()
return std.Command()
initialization()
return app.Command()
}
func Handle(method string, cb HandleFunc) {
initApplication()
std.Handle(method, cb)
initialization()
app.Handle(method, cb)
}

View File

@ -2,32 +2,42 @@ package kos
import (
"context"
"git.nspix.com/golang/kos/util/env"
"git.nspix.com/golang/kos/util/ip"
"git.nspix.com/golang/kos/util/sys"
"git.nobla.cn/golang/kos/util/env"
"git.nobla.cn/golang/kos/util/ip"
"git.nobla.cn/golang/kos/util/sys"
"os"
"strconv"
"strings"
"syscall"
)
type (
Options struct {
Name string
Version string
Address string
Port int
EnableDebug bool //开启调试模式
DisableHttp bool //禁用HTTP入口
DisableCommand bool //禁用命令行入口
DisableStateApi bool //禁用系统状态接口
Metadata map[string]string //原数据
Context context.Context
Signals []os.Signal
server Server
shortName string
Name string //名称
Version string //版本号
Address string //绑定地址
Port int //端口
EnableDebug bool //开启调试模式
DisableGateway bool //禁用HTTP和COMMAND入口
DisableHttp bool //禁用HTTP入口
EnableDirectHttp bool //启用HTTP直连模式
DisableCommand bool //禁用COMMAND入口
EnableDirectCommand bool //启用COMMAND直连模式
DisableStateApi bool //禁用系统状态接口
Metadata map[string]string //原数据
Context context.Context
Signals []os.Signal
server Server
shortName string
}
Option func(o *Options)
HandleOptions struct {
description string
}
HandleOption func(o *HandleOptions)
)
func (o *Options) ShortName() string {
@ -42,6 +52,12 @@ func (o *Options) ShortName() string {
return o.shortName
}
func WithHandleDescription(s string) HandleOption {
return func(o *HandleOptions) {
o.description = s
}
}
func WithName(name string, version string) Option {
return func(o *Options) {
o.Name = name
@ -49,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 {
return func(o *Options) {
o.Port = port
@ -67,7 +89,21 @@ func WithDebug() Option {
}
}
func NewOptions() *Options {
func WithDirectHttp() Option {
return func(o *Options) {
o.DisableCommand = true
o.EnableDirectHttp = true
}
}
func WithDirectCommand() Option {
return func(o *Options) {
o.DisableHttp = true
o.EnableDirectCommand = true
}
}
func NewOptions(cbs ...Option) *Options {
opts := &Options{
Name: env.Get(EnvAppName, sys.Hostname()),
Version: env.Get(EnvAppVersion, "0.0.1"),
@ -75,7 +111,19 @@ func NewOptions() *Options {
Metadata: make(map[string]string),
Signals: []os.Signal{syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL},
}
opts.Port = int(env.Integer(EnvAppPort, 80))
opts.Address = env.Get(EnvAppAddress, ip.Internal())
opts.Port = int(env.Integer(18080, EnvAppPort, "HTTP_PORT", "KOS_PORT"))
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
}

11
pkg/cache/cache.go vendored
View File

@ -5,9 +5,14 @@ import (
"time"
)
type (
LoadFunc func(ctx context.Context) ([]byte, error)
)
type Cache interface {
Set(ctx context.Context, key string, value any)
SetEx(ctx context.Context, key string, value any, expire time.Duration)
Get(ctx context.Context, key string) (value any, ok bool)
Set(ctx context.Context, key string, buf []byte)
SetEx(ctx context.Context, key string, buf []byte, expire time.Duration)
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)
}

51
pkg/cache/instance.go vendored
View File

@ -2,6 +2,8 @@ package cache
import (
"context"
"encoding/json"
"os"
"time"
)
@ -21,18 +23,57 @@ func GetCache() Cache {
return std
}
func Set(ctx context.Context, key string, value any) {
std.Set(ctx, key, value)
// Set 设置缓存数据
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) {
std.SetEx(ctx, key, value, expire)
// SetEx 设置一个有效时间的缓存数据
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)
}
// Del 删除缓存数据
func Del(ctx context.Context, key string) {
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
}

56
pkg/cache/memcache.go vendored
View File

@ -2,24 +2,66 @@ package cache
import (
"context"
"git.nobla.cn/golang/kos/util/env"
"github.com/patrickmn/go-cache"
"os"
"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 {
engine *cache.Cache
}
func (cache *MemCache) Set(ctx context.Context, key string, value any) {
cache.engine.Set(key, value, 0)
func (cache *MemCache) Try(ctx context.Context, key string, cb LoadFunc) (buf []byte, err error) {
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) {
cache.engine.Set(key, value, expire)
func (cache *MemCache) Set(ctx context.Context, key string, buf []byte) {
cache.engine.Set(key, buf, 0)
}
func (cache *MemCache) Get(ctx context.Context, key string) (value any, ok bool) {
return cache.engine.Get(key)
func (cache *MemCache) SetEx(ctx context.Context, key string, buf []byte, expire time.Duration) {
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) {
@ -28,6 +70,6 @@ func (cache *MemCache) Del(ctx context.Context, key string) {
func NewMemCache() *MemCache {
return &MemCache{
engine: cache.New(time.Hour, time.Minute*90),
engine: cache.New(memCacheDefaultExpired, memCacheCleanupInterval),
}
}

View File

@ -2,6 +2,8 @@ package log
import (
"fmt"
"git.nobla.cn/golang/kos/util/env"
"strconv"
"time"
)
@ -16,10 +18,23 @@ const (
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 {
Level int
EnableColor int
prefix string
Level int
DisableColor bool
DisableTime bool
prefix string
}
func (log *Console) SetLevel(lv int) {
@ -87,32 +102,29 @@ func (log *Console) Panicf(format string, args ...interface{}) {
}
func (log *Console) write(level int, s string) {
var ls string
if log.Level > level {
return
}
lvColor := map[int]int{
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 {
if log.DisableColor {
ls = getLevelText(level)
} else {
ls = fmt.Sprintf("\033[0m\033[%dm[%s]\033[0m", levelColor[level], getLevelText(level))
}
if log.prefix != "" {
ls += " [" + log.prefix + "]"
}
fmt.Println(time.Now().Format("2006-01-02 15:04:05") + " " + ls + " " + s)
if log.DisableTime {
fmt.Println(ls + " " + s)
} else {
fmt.Println(time.Now().Format("2006-01-02 15:04:05") + " " + ls + " " + s)
}
}
func NewConsoleLogger() *Console {
return &Console{
EnableColor: 1,
}
lg := &Console{}
lg.DisableColor, _ = strconv.ParseBool(env.Get("VOX_DISABLE_LOG_COLOR", "false"))
lg.DisableTime, _ = strconv.ParseBool(env.Get("VOX_DISABLE_LOG_DATETIME", "false"))
return lg
}

View File

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

View File

@ -0,0 +1,190 @@
package request
import (
"crypto/tls"
"io"
"net"
"net/http"
"net/http/cookiejar"
"strings"
"time"
)
type (
BeforeRequest func(req *http.Request) (err error)
AfterRequest func(req *http.Request, res *http.Response) (err error)
Client struct {
baseUrl string
Authorization Authorization
client *http.Client
cookieJar *cookiejar.Jar
interceptorRequest []BeforeRequest
interceptorResponse []AfterRequest
}
)
var (
DefaultClient = &http.Client{
Transport: &http.Transport{
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 {
var (
pos int
)
if len(urlPath) == 0 {
return client.baseUrl
}
if pos = strings.Index(urlPath, "//"); pos == -1 {
if client.baseUrl != "" {
if urlPath[0] != '/' {
urlPath = "/" + urlPath
}
return client.baseUrl + urlPath
}
}
return urlPath
}
func (client *Client) BeforeRequest(cb BeforeRequest) *Client {
client.interceptorRequest = append(client.interceptorRequest, cb)
return client
}
func (client *Client) AfterRequest(cb AfterRequest) *Client {
client.interceptorResponse = append(client.interceptorResponse, cb)
return client
}
func (client *Client) SetBaseUrl(s string) *Client {
client.baseUrl = strings.TrimSuffix(s, "/")
return client
}
func (client *Client) SetCookieJar(cookieJar *cookiejar.Jar) *Client {
client.client.Jar = cookieJar
return client
}
func (client *Client) SetClient(httpClient *http.Client) *Client {
client.client = httpClient
if client.cookieJar != nil {
client.client.Jar = client.cookieJar
}
return client
}
func (client *Client) SetTransport(transport http.RoundTripper) *Client {
client.client.Transport = transport
return client
}
func (client *Client) Get(urlPath string) *Request {
return newRequest(http.MethodGet, client.stashUri(urlPath), client)
}
func (client *Client) Put(urlPath string) *Request {
return newRequest(http.MethodPut, client.stashUri(urlPath), client)
}
func (client *Client) Post(urlPath string) *Request {
return newRequest(http.MethodPost, client.stashUri(urlPath), client)
}
func (client *Client) Delete(urlPath string) *Request {
return newRequest(http.MethodDelete, client.stashUri(urlPath), client)
}
func (client *Client) execute(r *Request) (res *http.Response, err error) {
var (
n int
reader io.Reader
)
if r.contentType == "" && r.body != nil {
r.contentType = r.detectContentType(r.body)
}
if r.body != nil {
if reader, err = r.readRequestBody(r.contentType, r.body); err != nil {
return
}
}
if r.rawRequest, err = http.NewRequest(r.method, r.uri, reader); err != nil {
return
}
for k, vs := range r.header {
for _, v := range vs {
r.rawRequest.Header.Add(k, v)
}
}
if r.contentType != "" {
r.rawRequest.Header.Set("Content-Type", r.contentType)
}
if client.Authorization != nil {
r.rawRequest.Header.Set("Authorization", client.Authorization.Token())
}
if r.context != nil {
r.rawRequest = r.rawRequest.WithContext(r.context)
}
n = len(client.interceptorRequest)
for i := n - 1; i >= 0; i-- {
if err = client.interceptorRequest[i](r.rawRequest); err != nil {
return
}
}
if r.rawResponse, err = client.client.Do(r.rawRequest); err != nil {
return nil, err
}
n = len(client.interceptorResponse)
for i := n - 1; i >= 0; i-- {
if err = client.interceptorResponse[i](r.rawRequest, r.rawResponse); err != nil {
_ = r.rawResponse.Body.Close()
return
}
}
return r.rawResponse, err
}
func New() *Client {
client := &Client{
client: DefaultClient,
interceptorRequest: make([]BeforeRequest, 0, 10),
interceptorResponse: make([]AfterRequest, 0, 10),
}
client.cookieJar, _ = cookiejar.New(nil)
client.client.Jar = client.cookieJar
return client
}

View File

@ -0,0 +1,14 @@
package request
import (
"bytes"
"testing"
)
func TestClient_execute(t *testing.T) {
c := New()
buf := []byte("Hello")
c.Post("https://ip.nspix.com/geo").
SetBody(bytes.NewReader(buf)).
Do()
}

View File

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

19
pkg/types/error.go 100644
View File

@ -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 //致命错误
)

View File

@ -5,12 +5,6 @@ import (
"errors"
"flag"
"fmt"
"git.nspix.com/golang/kos/entry"
"git.nspix.com/golang/kos/entry/cli"
"git.nspix.com/golang/kos/entry/http"
"git.nspix.com/golang/kos/pkg/log"
"git.nspix.com/golang/kos/util/env"
"github.com/sourcegraph/conc"
"net"
"net/http/pprof"
"os"
@ -21,6 +15,14 @@ import (
"sync/atomic"
"syscall"
"time"
"git.nobla.cn/golang/kos/entry"
"git.nobla.cn/golang/kos/entry/cli"
"git.nobla.cn/golang/kos/entry/http"
_ "git.nobla.cn/golang/kos/pkg/cache"
"git.nobla.cn/golang/kos/pkg/log"
"git.nobla.cn/golang/kos/util/env"
"github.com/sourcegraph/conc"
)
var (
@ -74,14 +76,15 @@ func (app *application) Command() *cli.Server {
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 {
app.http.Handle(http.MethodPost, path, func(ctx *http.Context) (err error) {
return cb(ctx)
})
}
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)
})
}
@ -91,7 +94,6 @@ func (app *application) httpServe() (err error) {
var (
l net.Listener
)
app.http = http.New(app.ctx)
if l, err = app.gateway.Apply(
entry.Feature(http.MethodGet),
entry.Feature(http.MethodHead),
@ -122,13 +124,14 @@ func (app *application) httpServe() (err error) {
app.waitGroup.Go(func() {
select {
case errChan <- app.http.Serve(l):
log.Infof("http server closed")
}
})
select {
case err = <-errChan:
case <-timer.C:
log.Infof("http server started")
if app.opts.EnableDirectHttp {
app.gateway.Direct(l)
}
}
return
}
@ -137,7 +140,6 @@ func (app *application) commandServe() (err error) {
var (
l net.Listener
)
app.command = cli.New(app.ctx)
if l, err = app.gateway.Apply(
cli.Feature,
); err != nil {
@ -149,13 +151,14 @@ func (app *application) commandServe() (err error) {
app.waitGroup.Go(func() {
select {
case errChan <- app.command.Serve(l):
log.Infof("command server closed")
}
})
select {
case err = <-errChan:
case <-timer.C:
log.Infof("command server started")
if app.opts.EnableDirectCommand {
app.gateway.Direct(l)
}
}
return
}
@ -208,20 +211,24 @@ func (app *application) preStart() (err error) {
app.Log().Infof("server starting")
env.Set(EnvAppName, app.opts.ShortName())
env.Set(EnvAppVersion, app.opts.Version)
addr = net.JoinHostPort(app.opts.Address, strconv.Itoa(app.opts.Port))
app.Log().Infof("server listen on: %s", addr)
app.gateway = entry.New(addr)
if err = app.gateway.Start(app.ctx); err != nil {
return
}
if !app.opts.DisableHttp {
if err = app.httpServe(); err != nil {
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))
app.Log().Infof("server listen on: %s", addr)
app.gateway = entry.New(addr)
if err = app.gateway.Start(app.ctx); err != nil {
return
}
}
if !app.opts.DisableCommand {
if err = app.commandServe(); err != nil {
return
if !app.opts.DisableHttp {
if err = app.httpServe(); err != nil {
return
}
}
if !app.opts.DisableCommand {
if err = app.commandServe(); err != nil {
return
}
}
}
app.plugins.Range(func(key, value any) bool {
@ -247,10 +254,24 @@ func (app *application) preStart() (err error) {
Uptime: time.Now().Sub(app.uptime).String(),
Gateway: app.gateway.State(),
})
})
}, WithHandleDescription("Display application state"))
app.Handle("/-/healthy", func(ctx Context) (err error) {
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 {
if plugin, ok := value.(Plugin); ok {
@ -269,6 +290,11 @@ func (app *application) preStop() (err error) {
return
}
app.Log().Infof("server stopping")
if app.opts.server != nil {
if err = app.opts.server.Stop(); err != nil {
app.Log().Warnf("app server stop error: %s", err.Error())
}
}
app.cancelFunc(ErrStopping)
app.plugins.Range(func(key, value any) bool {
if plugin, ok := value.(Plugin); ok {
@ -278,18 +304,20 @@ func (app *application) preStop() (err error) {
}
return true
})
if app.http != nil {
if err = app.http.Shutdown(); err != nil {
app.Log().Warnf("server http shutdown error: %s", err.Error())
if !app.opts.DisableGateway {
if app.http != nil {
if err = app.http.Shutdown(); err != nil {
app.Log().Warnf("server http shutdown error: %s", err.Error())
}
}
}
if app.command != nil {
if err = app.command.Shutdown(); err != nil {
app.Log().Warnf("server command shutdown error: %s", err.Error())
if app.command != nil {
if err = app.command.Shutdown(); err != nil {
app.Log().Warnf("server command shutdown error: %s", err.Error())
}
}
if err = app.gateway.Stop(); err != nil {
app.Log().Warnf("server gateway shutdown error: %s", err.Error())
}
}
if err = app.gateway.Stop(); err != nil {
app.Log().Warnf("server gateway shutdown error: %s", err.Error())
}
app.plugins.Range(func(key, value any) bool {
if plugin, ok := value.(Plugin); ok {
@ -334,11 +362,8 @@ func (app *application) Run() (err error) {
return app.preStop()
}
func New(cbs ...Option) *application {
opts := NewOptions()
for _, cb := range cbs {
cb(opts)
}
func New(cbs ...Option) Application {
opts := NewOptions(cbs...)
app := &application{
opts: opts,
uptime: time.Now(),

View File

@ -2,9 +2,9 @@ package kos
import (
"context"
"git.nspix.com/golang/kos/entry"
"git.nspix.com/golang/kos/entry/cli"
"git.nspix.com/golang/kos/entry/http"
"git.nobla.cn/golang/kos/entry"
"git.nobla.cn/golang/kos/entry/cli"
"git.nobla.cn/golang/kos/entry/http"
)
type (
@ -22,9 +22,10 @@ type (
Info() *Info
Http() *http.Server
Command() *cli.Server
Handle(method string, cb HandleFunc)
Handle(method string, cb HandleFunc, opts ...HandleOption)
Run() (err error)
}
// Info application information
Info struct {
ID string `json:"id"`

View File

@ -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
}

View File

@ -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)
}
})
}
}

12
util/bs/safe.go 100644
View File

@ -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)
}

23
util/bs/unsafe.go 100644
View File

@ -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)},
))
}

View File

@ -1,4 +1,4 @@
package crypto
package aes
import (
"bytes"

23
util/env/env.go vendored
View File

@ -15,7 +15,19 @@ func Get(name string, val string) string {
}
}
func Integer(name string, val int64) int64 {
func Getter(val string, names ...string) string {
var (
value string
)
for _, name := range names {
if value = strings.TrimSpace(os.Getenv(name)); value != "" {
return value
}
}
return val
}
func Int(name string, val int64) int64 {
value := Get(name, "")
if n, err := strconv.ParseInt(value, 10, 64); err == nil {
return n
@ -24,6 +36,15 @@ func Integer(name string, val int64) int64 {
}
}
func Integer(val int64, names ...string) int64 {
value := Getter("", names...)
if n, err := strconv.ParseInt(value, 10, 64); err == nil {
return n
} else {
return val
}
}
func Float(name string, val float64) float64 {
value := Get(name, "")
if n, err := strconv.ParseFloat(value, 64); err == nil {

View File

@ -14,11 +14,13 @@ import (
"path"
"strings"
"time"
"git.nobla.cn/golang/kos/util/env"
)
var (
httpClient = http.Client{
Timeout: time.Second * 15,
Timeout: time.Second * 30,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
@ -29,7 +31,7 @@ var (
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: false,
MaxIdleConns: 10,
MaxIdleConns: 48,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
@ -37,6 +39,36 @@ 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) {
var (
buf []byte
)
switch v := data.(type) {
case string:
r = strings.NewReader(v)
contentType = "x-www-form-urlencoded"
case []byte:
r = bytes.NewReader(v)
contentType = "x-www-form-urlencoded"
default:
if buf, err = json.Marshal(v); err == nil {
r = bytes.NewReader(buf)
contentType = "application/json"
}
}
return
}
// Get performs a GET request to the specified URL with optional parameters and headers.
func Get(ctx context.Context, urlString string, cbs ...Option) (res *http.Response, err error) {
var (
uri *url.URL
@ -56,7 +88,7 @@ func Get(ctx context.Context, urlString string, cbs ...Option) (res *http.Respon
}
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
}
if opts.Header != nil {
@ -64,12 +96,12 @@ func Get(ctx context.Context, urlString string, cbs ...Option) (res *http.Respon
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) {
var (
buf []byte
uri *url.URL
req *http.Request
contentType string
@ -90,23 +122,11 @@ func Post(ctx context.Context, urlString string, cbs ...Option) (res *http.Respo
uri.RawQuery = qs.Encode()
}
if opts.Data != nil {
switch v := opts.Data.(type) {
case string:
reader = strings.NewReader(v)
contentType = "x-www-form-urlencoded"
case []byte:
reader = bytes.NewReader(v)
contentType = "x-www-form-urlencoded"
default:
if buf, err = json.Marshal(v); err == nil {
reader = bytes.NewReader(buf)
contentType = "application/json"
} else {
return
}
if reader, contentType, err = encode(opts.Data); err != nil {
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
}
if opts.Header != nil {
@ -117,16 +137,17 @@ func Post(ctx context.Context, urlString string, cbs ...Option) (res *http.Respo
if 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 (
buf []byte
contentType string
reader io.Reader
uri *url.URL
res *http.Response
req *http.Request
contentType string
)
opts := newOptions()
for _, cb := range cbs {
@ -142,7 +163,12 @@ func Request(ctx context.Context, urlString string, response any, cbs ...Option)
}
uri.RawQuery = qs.Encode()
}
if req, err = http.NewRequest(http.MethodGet, uri.String(), nil); err != nil {
if opts.Data != nil {
if reader, contentType, err = encode(opts.Data); err != nil {
return
}
}
if req, err = http.NewRequestWithContext(ctx, opts.Method, uri.String(), reader); err != nil {
return
}
if opts.Header != nil {
@ -150,33 +176,44 @@ func Request(ctx context.Context, urlString string, response any, cbs ...Option)
req.Header.Set(k, v)
}
}
if res, err = do(ctx, req, opts); err != nil {
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
if res, err = do(req, opts); err != nil {
return
}
defer func() {
_ = res.Body.Close()
}()
if res.StatusCode != http.StatusOK {
if buf, err = io.ReadAll(res.Body); err == nil && len(buf) > 0 {
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)
}
err = fmt.Errorf("ubexpected HTTP status code: %d", res.StatusCode)
return
}
//don't care response
if result == nil {
return nil
}
contentType = strings.ToLower(res.Header.Get("Content-Type"))
extName := path.Ext(req.URL.String())
if strings.Contains(contentType, JSON) || extName == ".json" {
err = json.NewDecoder(res.Body).Decode(response)
err = json.NewDecoder(res.Body).Decode(result)
} else if strings.Contains(contentType, XML) || extName == ".xml" {
err = xml.NewDecoder(res.Body).Decode(response)
err = xml.NewDecoder(res.Body).Decode(result)
} else {
err = fmt.Errorf("unsupported content type: %s", contentType)
}
return
}
func do(ctx context.Context, req *http.Request, opts *Options) (res *http.Response, err error) {
func Do(req *http.Request, cbs ...Option) (res *http.Response, err error) {
opts := newOptions()
for _, cb := range cbs {
cb(opts)
}
return do(req, opts)
}
func do(req *http.Request, opts *Options) (res *http.Response, err error) {
if opts.Human {
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.54")
@ -184,9 +221,6 @@ func do(ctx context.Context, req *http.Request, opts *Options) (res *http.Respon
if req.Header.Get("Referer") == "" {
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.WithContext(ctx))
return httpClient.Do(req)
}

View File

@ -1,7 +1,6 @@
package fs
import (
"errors"
"os"
)
@ -11,22 +10,26 @@ func IsDir(filename string) (bool, error) {
if err != nil {
return false, err
}
fm := fd.Mode()
return fm.IsDir(), nil
return fd.Mode().IsDir(), nil
}
// DirectoryOrCreate checking directory, is not exists will create
func DirectoryOrCreate(dirname string) error {
// IsFile Tells whether the filename is a file
func IsFile(filename string) (bool, error) {
fd, err := os.Stat(filename)
if err != nil {
return false, 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 {
if errors.Is(err, os.ErrNotExist) {
return os.MkdirAll(dirname, 0755)
} else {
return err
}
return os.MkdirAll(dirname, perm)
} else {
if fi.IsDir() {
return nil
}
return errors.New("file not directory")
return os.ErrPermission
}
}

View File

@ -1 +0,0 @@
package fs

View File

@ -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)
}

View File

@ -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###,##" => "12345,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))
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -1,41 +1,49 @@
package reflect
import (
"errors"
"fmt"
"reflect"
"strconv"
"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 {
var (
pos int
tagValue string
refValue reflect.Value
refType reflect.Type
fieldType reflect.StructField
allowTags = []string{"json", "yaml", "xml"}
)
refValue = v.FieldByName(field)
if !refValue.IsValid() {
refType = v.Type()
for i := 0; i < refType.NumField(); i++ {
fieldType = refType.Field(i)
for _, tagName := range allowTags {
tagValue = fieldType.Tag.Get(tagName)
if tagValue == "" {
continue
}
if pos = strings.Index(tagValue, ","); pos != -1 {
tagValue = tagValue[:pos]
}
if tagValue == field {
return v.Field(i)
}
refType = v.Type()
for i := 0; i < refType.NumField(); i++ {
fieldType = refType.Field(i)
for _, tagName := range allowTags {
tagValue = fieldType.Tag.Get(tagName)
if tagValue == "" {
continue
}
if pos = strings.IndexByte(tagValue, ','); pos != -1 {
tagValue = tagValue[:pos]
}
if tagValue == field {
return v.Field(i)
}
}
}
return refValue
return v.FieldByName(field)
}
func Assign(variable reflect.Value, value interface{}) (err error) {
return safeAssignment(variable, value)
}
func safeAssignment(variable reflect.Value, value interface{}) (err error) {
@ -52,8 +60,45 @@ func safeAssignment(variable reflect.Value, value interface{}) (err error) {
return
}
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:
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:
variable.SetInt(rv.Int())
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)
}
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:
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:
variable.SetUint(uint64(rv.Int()))
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)
}
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:
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:
variable.SetFloat(float64(rv.Int()))
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)
}
default:
err = fmt.Errorf("unsupported kind %s", rv.Kind())
err = fmt.Errorf("decimal value can not assign %s", rv.Kind())
}
case reflect.String:
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:
variable.SetString(strconv.FormatInt(rv.Int(), 10))
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:
variable.SetString(fmt.Sprint(value))
}
case reflect.Interface:
variable.Set(rv)
default:
err = fmt.Errorf("unsupported kind %s", kind)
}
@ -139,20 +204,135 @@ func Set(hacky interface{}, field string, value interface{}) (err error) {
return
}
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:
n = 0
innerType := refField.Type().Elem()
if rv.Kind() == reflect.Array || rv.Kind() == reflect.Slice {
sliceVar := reflect.MakeSlice(refField.Type(), rv.Len(), rv.Len())
n = 0
for i := 0; i < rv.Len(); i++ {
srcVal := rv.Index(i)
dstVal := reflect.New(innerType).Elem()
if err = safeAssignment(dstVal, srcVal); err == nil {
if innerType.Kind() == reflect.Struct {
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)
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)
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:
err = safeAssignment(refField, value)

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -2,19 +2,15 @@ package sys
import (
"os"
"path/filepath"
"runtime"
)
// HomeDir return user home directory
func HomeDir() string {
if runtime.GOOS == "windows" {
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
if dirname, err := os.UserHomeDir(); err == nil {
return dirname
}
if h := os.Getenv("HOME"); h != "" {
return h
}
return "/"
return os.TempDir()
}
// HiddenFile get hidden file prefix
@ -29,20 +25,12 @@ func HiddenFile(name string) string {
// CacheDir return user cache directory
func CacheDir() string {
switch runtime.GOOS {
case "darwin":
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
}
}
// Worst case:
return HomeDir()
if dirname, err := os.UserCacheDir(); err == nil {
return dirname
}
if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
return xdg
}
return filepath.Join(HomeDir(), ".cache")
return os.TempDir()
}
func TempFile() (*os.File, error) {
return os.CreateTemp(os.TempDir(), "kos_*")
}