Compare commits

..

34 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
43 changed files with 1882 additions and 236 deletions

View File

@ -4,8 +4,13 @@ import (
"context"
"embed"
"flag"
httpkg "net/http"
"time"
"git.nspix.com/golang/kos"
"git.nobla.cn/golang/kos/entry/cli"
"git.nobla.cn/golang/kos/entry/http"
"git.nobla.cn/golang/kos"
)
//go:embed web
@ -14,8 +19,39 @@ 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
}
@ -26,9 +62,8 @@ func (s *subServer) Stop() (err error) {
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{}),
kos.WithDirectHttp(),
)
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

@ -10,9 +10,10 @@ import (
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"git.nspix.com/golang/kos/util/env"
"git.nobla.cn/golang/kos/util/env"
"github.com/sourcegraph/conc"
)
@ -29,6 +30,7 @@ type Server struct {
middleware []Middleware
router *Router
l net.Listener
exitFlag int32
}
func (svr *Server) applyContext() *Context {
@ -44,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
@ -54,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))
@ -75,6 +85,7 @@ func (svr *Server) handle(ctx *Context, frame *Frame) {
ctx.setParam(params)
err = r.command.Handle(ctx)
}
return
}
func (svr *Server) nextSequence() int64 {
@ -130,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
}
@ -189,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

@ -28,16 +28,27 @@ type Context struct {
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
)
@ -48,10 +59,18 @@ func (ctx *Context) RealIp() string {
}
ipaddr, _, _ = net.SplitHostPort(ctx.Request().RemoteAddr)
__end:
if pos = strings.LastIndexByte(ipaddr, ','); pos > -1 {
ipaddr = ipaddr[:pos]
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 ipaddr
return strings.TrimSpace(ipaddr)
}
func (ctx *Context) Request() *http.Request {
@ -74,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) 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")
}
@ -111,7 +143,7 @@ func (ctx *Context) Error(code int, reason string) (err error) {
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)
}
@ -120,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

@ -1,13 +1,21 @@
package http
import "git.nobla.cn/golang/kos/pkg/types"
const (
ErrAccessDenied = 4003 //拒绝访问
ErrPermissionDenied = 4004 //没有权限
ErrInvalidRequest = 4005 //请求无效或不合法
ErrInvalidPayload = 4006 //请求数据无效
ErrResourceCreate = 8001 //资源创建失败
ErrResourceUpdate = 8002 //资源更新失败
ErrResourceDelete = 8003 //资源删除失败
ErrResourceNotFound = 8004 //资源未找到
ErrTemporaryUnavailable = 8006 //临时性不可用
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 //致命错误
)

View File

@ -3,13 +3,19 @@ package http
import (
"io/fs"
"net/http"
"os"
"path"
"strings"
"time"
)
type (
FS struct {
fs http.FileSystem
modtime time.Time
fs http.FileSystem
modtime time.Time
prefix string
indexFile string
denyDirectory bool
}
File struct {
@ -74,11 +80,54 @@ func (file *File) Stat() (fs.FileInfo, error) {
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
}

View File

@ -3,12 +3,13 @@ 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"
)
@ -17,12 +18,16 @@ var (
)
type Server struct {
ctx context.Context
serve *http.Server
router *router.Router
middleware []Middleware
uptime time.Time
anyRequests map[string]http.Handler
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 {
@ -41,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)
}
@ -61,6 +75,10 @@ func (svr *Server) wrapHandle(cb HandleFunc, middleware ...Middleware) router.Ha
}
}
func (svr *Server) Before(cb ...HandleFunc) {
svr.beforeRequests = append(svr.beforeRequests, cb...)
}
func (svr *Server) Use(middleware ...Middleware) {
svr.middleware = append(svr.middleware, middleware...)
}
@ -91,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") {
@ -159,12 +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)
@ -179,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)
}
@ -191,11 +234,12 @@ func (svr *Server) Shutdown() (err error) {
func New(ctx context.Context) *Server {
svr := &Server{
ctx: ctx,
uptime: time.Now(),
router: router.New(),
anyRequests: make(map[string]http.Handler),
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")
}
}

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,48 +1,83 @@
package kos
import (
"git.nspix.com/golang/kos/entry/cli"
"git.nspix.com/golang/kos/entry/http"
_ "git.nspix.com/golang/kos/pkg/request"
_ "git.nspix.com/golang/kos/util/bs"
_ "git.nspix.com/golang/kos/util/fetch"
_ "git.nspix.com/golang/kos/util/random"
_ "git.nspix.com/golang/kos/util/reflection"
"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,25 +2,27 @@ 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
Name string //名称
Version string //版本号
Address string //绑定地址
Port int //端口
EnableDebug bool //开启调试模式
DisableGateway bool //禁用HTTP和COMMAND入口
DisableHttp bool //禁用HTTP入口
EnableDirectHttp bool //启用HTTP直连模式
DisableCommand bool //禁用命令行入口
EnableDirectCommand bool //启用命令行直连模式
DisableCommand bool //禁用COMMAND入口
EnableDirectCommand bool //启用COMMAND直连模式
DisableStateApi bool //禁用系统状态接口
Metadata map[string]string //原数据
Context context.Context
@ -30,6 +32,12 @@ type (
}
Option func(o *Options)
HandleOptions struct {
description string
}
HandleOption func(o *HandleOptions)
)
func (o *Options) ShortName() string {
@ -44,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
@ -51,6 +65,12 @@ func WithName(name string, version string) Option {
}
}
func WithoutGateway() Option {
return func(o *Options) {
o.DisableGateway = true
}
}
func WithPort(port int) Option {
return func(o *Options) {
o.Port = port
@ -83,7 +103,7 @@ func WithDirectCommand() Option {
}
}
func NewOptions() *Options {
func NewOptions(cbs ...Option) *Options {
opts := &Options{
Name: env.Get(EnvAppName, sys.Hostname()),
Version: env.Get(EnvAppVersion, "0.0.1"),
@ -93,5 +113,17 @@ func NewOptions() *Options {
}
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
}

10
pkg/cache/cache.go vendored
View File

@ -6,13 +6,13 @@ import (
)
type (
LoadFunc func(ctx context.Context) (any, error)
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)
Try(ctx context.Context, key string, cb LoadFunc) (value any, err error)
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)
}

49
pkg/cache/instance.go vendored
View File

@ -2,6 +2,8 @@ package cache
import (
"context"
"encoding/json"
"os"
"time"
)
@ -21,22 +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 Try(ctx context.Context, key string, cb LoadFunc) (value any, err error) {
// Try 尝试获取缓存数据,获取不到就设置
func Try(ctx context.Context, key string, cb LoadFunc) (buf []byte, err error) {
return std.Try(ctx, key, cb)
}
func Get(ctx context.Context, key string) (value any, ok bool) {
// 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
}

30
pkg/cache/memcache.go vendored
View File

@ -2,7 +2,7 @@ package cache
import (
"context"
"git.nspix.com/golang/kos/util/env"
"git.nobla.cn/golang/kos/util/env"
"github.com/patrickmn/go-cache"
"os"
"time"
@ -30,32 +30,38 @@ type MemCache struct {
engine *cache.Cache
}
func (cache *MemCache) Try(ctx context.Context, key string, cb LoadFunc) (value any, err error) {
func (cache *MemCache) Try(ctx context.Context, key string, cb LoadFunc) (buf []byte, err error) {
var (
ok bool
)
if value, ok = cache.engine.Get(key); ok {
return value, nil
if buf, ok = cache.Get(ctx, key); ok {
return buf, nil
}
if cb == nil {
return nil, os.ErrNotExist
}
if value, err = cb(ctx); err == nil {
cache.engine.Set(key, value, 0)
if buf, err = cb(ctx); err == nil {
cache.Set(ctx, key, buf)
}
return
}
func (cache *MemCache) Set(ctx context.Context, key string, value any) {
cache.engine.Set(key, value, 0)
func (cache *MemCache) Set(ctx context.Context, key string, buf []byte) {
cache.engine.Set(key, buf, 0)
}
func (cache *MemCache) SetEx(ctx context.Context, key string, value any, expire time.Duration) {
cache.engine.Set(key, value, expire)
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) (value any, ok bool) {
return cache.engine.Get(key)
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) {

View File

@ -2,7 +2,7 @@ package log
import (
"fmt"
"git.nspix.com/golang/kos/util/env"
"git.nobla.cn/golang/kos/util/env"
"strconv"
"time"
)

View File

@ -1,10 +1,13 @@
package request
import (
"crypto/tls"
"io"
"net"
"net/http"
"net/http/cookiejar"
"strings"
"time"
)
type (
@ -21,6 +24,44 @@ type (
}
)
var (
DefaultClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
ForceAttemptHTTP2: true,
MaxIdleConns: 64,
MaxIdleConnsPerHost: 8,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
Timeout: time.Second * 30,
}
UnsafeClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
ForceAttemptHTTP2: true,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 64,
MaxIdleConnsPerHost: 8,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
Timeout: time.Second * 30,
}
)
func (client *Client) stashUri(urlPath string) string {
var (
pos int
@ -139,7 +180,7 @@ func (client *Client) execute(r *Request) (res *http.Response, err error) {
func New() *Client {
client := &Client{
client: http.DefaultClient,
client: DefaultClient,
interceptorRequest: make([]BeforeRequest, 0, 10),
interceptorResponse: make([]AfterRequest, 0, 10),
}

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

@ -16,12 +16,12 @@ import (
"syscall"
"time"
"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/cache"
"git.nspix.com/golang/kos/pkg/log"
"git.nspix.com/golang/kos/util/env"
"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"
)
@ -76,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)
})
}
@ -93,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),
@ -140,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 {
@ -212,23 +211,26 @@ 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 {
if plugin, ok := value.(Plugin); ok {
if err = plugin.BeforeStart(); err != nil {
@ -252,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 {
@ -288,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 {
@ -344,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,7 +22,8 @@ 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

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

View File

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

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,15 @@ var (
}
)
func init() {
httpDefaultTimeout := env.Get("HTTP_CLIENT_TIMEOUT", "30s")
if httpDefaultTimeout != "" {
if duration, err := time.ParseDuration(httpDefaultTimeout); err == nil {
httpClient.Timeout = duration
}
}
}
func encode(data any) (r io.Reader, contentType string, err error) {
var (
buf []byte
@ -57,6 +68,7 @@ func encode(data any) (r io.Reader, contentType string, err error) {
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
@ -76,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 {
@ -84,9 +96,10 @@ 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 (
uri *url.URL
@ -113,7 +126,7 @@ func Post(ctx context.Context, urlString string, cbs ...Option) (res *http.Respo
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 {
@ -124,17 +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
reader io.Reader
)
opts := newOptions()
for _, cb := range cbs {
@ -155,7 +168,7 @@ func Request(ctx context.Context, urlString string, response any, cbs ...Option)
return
}
}
if req, err = http.NewRequest(opts.Method, uri.String(), reader); err != nil {
if req, err = http.NewRequestWithContext(ctx, opts.Method, uri.String(), reader); err != nil {
return
}
if opts.Header != nil {
@ -166,41 +179,41 @@ func Request(ctx context.Context, urlString string, response any, cbs ...Option)
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
if res, err = do(ctx, req, opts); err != nil {
if res, err = do(req, opts); err != nil {
return
}
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, cbs ...Option) (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(ctx, req, opts)
return do(req, opts)
}
func do(ctx context.Context, req *http.Request, opts *Options) (res *http.Response, err error) {
func do(req *http.Request, opts *Options) (res *http.Response, err error) {
if opts.Human {
if 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")
@ -208,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,6 +1,7 @@
package reflect
import (
"errors"
"fmt"
"reflect"
"strconv"
@ -11,6 +12,10 @@ 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
@ -37,6 +42,10 @@ func findField(v reflect.Value, field string) reflect.Value {
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) {
var (
n int64
@ -51,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:
@ -64,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:
@ -79,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:
@ -94,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:
@ -109,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)
}
@ -138,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

@ -1,8 +1,11 @@
package reflection
import "git.nspix.com/golang/kos/util/reflect"
import (
"git.nobla.cn/golang/kos/util/reflect"
reflectpkg "reflect"
)
func Setter(hacky any, variables map[string]any) (err error) {
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
@ -11,6 +14,10 @@ func Setter(hacky any, variables map[string]any) (err error) {
return
}
func Set(hacky any, field string, value interface{}) (err error) {
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_*")
}