init project
This commit is contained in:
commit
db3e80dcf7
|
@ -0,0 +1,47 @@
|
|||
bin/
|
||||
|
||||
.svn/
|
||||
.godeps
|
||||
./build
|
||||
.cover/
|
||||
dist
|
||||
_site
|
||||
_posts
|
||||
*.dat
|
||||
*.db
|
||||
.DS_Store
|
||||
.vscode
|
||||
vendor
|
||||
|
||||
|
||||
# Go.gitignore
|
||||
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
storage
|
||||
.idea
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
|
||||
profile
|
||||
|
||||
# vim stuff
|
||||
*.sw[op]
|
|
@ -0,0 +1,34 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"flag"
|
||||
"git.nspix.com/golang/kos"
|
||||
"git.nspix.com/golang/kos/pkg/log"
|
||||
)
|
||||
|
||||
//go:embed web
|
||||
var webDir embed.FS
|
||||
|
||||
type subServer struct {
|
||||
}
|
||||
|
||||
func (s *subServer) Start(ctx context.Context) (err error) {
|
||||
kos.Http().Embed("/ui/web", "web", webDir)
|
||||
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.WithServer(&subServer{}),
|
||||
)
|
||||
svr.Run()
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
h1{
|
||||
font-size: 20px;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
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">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,15 @@
|
|||
package kos
|
||||
|
||||
const (
|
||||
EnvAppName = "VOX_NAME"
|
||||
EnvAppVersion = "VOX_VERSION"
|
||||
EnvAppPort = "VOX_PORT"
|
||||
EnvAppAddress = "VOX_ADDRESS"
|
||||
)
|
||||
|
||||
const (
|
||||
StateHealthy = "Healthy"
|
||||
StateNoAccepting = "NoAccepting"
|
||||
StateNoProgress = "NoProgress"
|
||||
StateUnavailable = "Unavailable"
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
package kos
|
||||
|
||||
type Context interface {
|
||||
Bind(v any) (err error)
|
||||
Param(s string) string
|
||||
Success(v any) (err error)
|
||||
Error(code int, reason string) (err error)
|
||||
}
|
|
@ -0,0 +1,257 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/peterh/liner"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
address string
|
||||
sequence uint16
|
||||
conn net.Conn
|
||||
liner *liner.State
|
||||
mutex sync.Mutex
|
||||
exitChan chan struct{}
|
||||
readyChan chan struct{}
|
||||
commandChan chan *Frame
|
||||
completerChan chan *Frame
|
||||
Timeout time.Duration
|
||||
exitFlag int32
|
||||
}
|
||||
|
||||
func (client *Client) getSequence() uint16 {
|
||||
client.mutex.Lock()
|
||||
defer client.mutex.Unlock()
|
||||
if client.sequence >= math.MaxUint16 {
|
||||
client.sequence = 0
|
||||
}
|
||||
client.sequence++
|
||||
n := client.sequence
|
||||
return n
|
||||
}
|
||||
|
||||
func (client *Client) dialContext(ctx context.Context, address string) (conn net.Conn, err error) {
|
||||
var (
|
||||
pos int
|
||||
network string
|
||||
dialer net.Dialer
|
||||
)
|
||||
if pos = strings.Index(address, "://"); pos > -1 {
|
||||
network = address[:pos]
|
||||
address = address[pos+3:]
|
||||
} else {
|
||||
network = "tcp"
|
||||
}
|
||||
if conn, err = dialer.DialContext(ctx, network, address); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) renderBanner(info *Info) {
|
||||
client.name = info.Name
|
||||
fmt.Printf("Welcome to the %s(%s) monitor\n", info.Name, info.Version)
|
||||
fmt.Printf("Your connection id is %d\n", info.ID)
|
||||
fmt.Printf("Last login: %s from %s\n", info.ServerTime.Format(time.RFC822), info.RemoteAddr)
|
||||
fmt.Printf("Type 'help' for help. Type 'exit' for quit. Type 'cls' to clear input statement.\n")
|
||||
}
|
||||
|
||||
func (client *Client) ioLoop(r io.Reader) {
|
||||
defer func() {
|
||||
_ = client.Close()
|
||||
}()
|
||||
for {
|
||||
frame, err := readFrame(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch frame.Type {
|
||||
case PacketTypeHandshake:
|
||||
info := &Info{}
|
||||
if err = json.Unmarshal(frame.Data, info); err == nil {
|
||||
client.renderBanner(info)
|
||||
}
|
||||
select {
|
||||
case client.readyChan <- struct{}{}:
|
||||
case <-client.exitChan:
|
||||
return
|
||||
}
|
||||
case PacketTypeCompleter:
|
||||
select {
|
||||
case client.completerChan <- frame:
|
||||
case <-client.exitChan:
|
||||
return
|
||||
}
|
||||
case PacketTypeCommand:
|
||||
select {
|
||||
case client.commandChan <- frame:
|
||||
case <-client.exitChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) waitResponse(seq uint16, timeout time.Duration) {
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
fmt.Println("timeout waiting for response")
|
||||
return
|
||||
case <-client.exitChan:
|
||||
return
|
||||
case res, ok := <-client.commandChan:
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if res.Seq == seq {
|
||||
if res.Error != "" {
|
||||
fmt.Print(res.Error)
|
||||
} else {
|
||||
fmt.Print(string(res.Data))
|
||||
}
|
||||
if res.Flag == FlagComplete {
|
||||
fmt.Println("")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) completer(str string) (ss []string) {
|
||||
var (
|
||||
err error
|
||||
seq uint16
|
||||
)
|
||||
ss = make([]string, 0)
|
||||
seq = client.getSequence()
|
||||
if err = writeFrame(client.conn, newFrame(PacketTypeCompleter, FlagComplete, seq, []byte(str))); err != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-time.After(time.Second * 5):
|
||||
case frame, ok := <-client.completerChan:
|
||||
if ok {
|
||||
err = json.Unmarshal(frame.Data, &ss)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) Execute(s string) (err error) {
|
||||
var (
|
||||
seq uint16
|
||||
)
|
||||
if client.conn, err = client.dialContext(client.ctx, client.address); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = client.Close()
|
||||
}()
|
||||
go client.ioLoop(client.conn)
|
||||
seq = client.getSequence()
|
||||
if err = writeFrame(client.conn, newFrame(PacketTypeCommand, FlagComplete, seq, []byte(s))); err != nil {
|
||||
return err
|
||||
}
|
||||
client.waitResponse(seq, time.Second*30)
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) Shell() (err error) {
|
||||
var (
|
||||
seq uint16
|
||||
line string
|
||||
)
|
||||
client.liner.SetCtrlCAborts(true)
|
||||
if client.conn, err = client.dialContext(client.ctx, client.address); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = client.Close()
|
||||
}()
|
||||
if err = writeFrame(client.conn, newFrame(PacketTypeHandshake, FlagComplete, client.getSequence(), nil)); err != nil {
|
||||
return
|
||||
}
|
||||
go client.ioLoop(client.conn)
|
||||
select {
|
||||
case <-client.readyChan:
|
||||
case <-client.ctx.Done():
|
||||
return
|
||||
}
|
||||
client.liner.SetCompleter(client.completer)
|
||||
for {
|
||||
if line, err = client.liner.Prompt(client.name + "> "); err != nil {
|
||||
break
|
||||
}
|
||||
if atomic.LoadInt32(&client.exitFlag) == 1 {
|
||||
fmt.Println(Bye)
|
||||
break
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if strings.ToLower(line) == "exit" || strings.ToLower(line) == "quit" {
|
||||
fmt.Println(Bye)
|
||||
return
|
||||
}
|
||||
if strings.ToLower(line) == "clear" || strings.ToLower(line) == "cls" {
|
||||
fmt.Print("\033[2J")
|
||||
continue
|
||||
}
|
||||
seq = client.getSequence()
|
||||
if err = writeFrame(client.conn, newFrame(PacketTypeCommand, FlagComplete, seq, []byte(line))); err != nil {
|
||||
break
|
||||
}
|
||||
client.liner.AppendHistory(line)
|
||||
client.waitResponse(seq, client.Timeout)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) Close() (err error) {
|
||||
if !atomic.CompareAndSwapInt32(&client.exitFlag, 0, 1) {
|
||||
return
|
||||
}
|
||||
close(client.exitChan)
|
||||
if client.conn != nil {
|
||||
err = client.conn.Close()
|
||||
}
|
||||
if client.liner != nil {
|
||||
err = client.liner.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, addr string) *Client {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return &Client{
|
||||
ctx: ctx,
|
||||
address: addr,
|
||||
name: filepath.Base(os.Args[0]),
|
||||
Timeout: time.Second * 30,
|
||||
liner: liner.NewLiner(),
|
||||
readyChan: make(chan struct{}, 1),
|
||||
exitChan: make(chan struct{}),
|
||||
commandChan: make(chan *Frame, 5),
|
||||
completerChan: make(chan *Frame, 5),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package cli
|
||||
|
||||
var (
|
||||
Feature = []byte{67, 76, 73}
|
||||
OK = []byte("OK")
|
||||
Bye = "Bye Bye"
|
||||
)
|
||||
|
||||
const (
|
||||
errNotFound = 4004
|
||||
errExecuteFailed = 4005
|
||||
)
|
|
@ -0,0 +1,110 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
Id int64
|
||||
seq uint16
|
||||
wc io.WriteCloser
|
||||
params map[string]string
|
||||
args []string
|
||||
}
|
||||
|
||||
func (ctx *Context) reset(id int64, wc io.WriteCloser) {
|
||||
ctx.Id = id
|
||||
ctx.wc = wc
|
||||
ctx.seq = 0
|
||||
ctx.args = make([]string, 0)
|
||||
ctx.params = make(map[string]string)
|
||||
}
|
||||
|
||||
func (ctx *Context) setArgs(args []string) {
|
||||
ctx.args = args
|
||||
}
|
||||
|
||||
func (ctx *Context) setParam(ps map[string]string) {
|
||||
ctx.params = ps
|
||||
}
|
||||
|
||||
func (ctx *Context) Bind(v any) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (ctx *Context) Argument(index int) string {
|
||||
if index >= len(ctx.args) || index < 0 {
|
||||
return ""
|
||||
}
|
||||
return ctx.args[index]
|
||||
}
|
||||
|
||||
func (ctx *Context) Param(s string) string {
|
||||
if v, ok := ctx.params[s]; ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ctx *Context) Success(v any) (err error) {
|
||||
return ctx.send(responsePayload{Type: PacketTypeCommand, Data: v})
|
||||
}
|
||||
|
||||
func (ctx *Context) Error(code int, reason string) (err error) {
|
||||
return ctx.send(responsePayload{Type: PacketTypeCommand, Code: code, Reason: reason})
|
||||
}
|
||||
|
||||
func (ctx *Context) Close() (err error) {
|
||||
return ctx.wc.Close()
|
||||
}
|
||||
|
||||
func (ctx *Context) send(res responsePayload) (err error) {
|
||||
var (
|
||||
ok bool
|
||||
buf []byte
|
||||
marshal encoder
|
||||
)
|
||||
if res.Code > 0 {
|
||||
err = writeFrame(ctx.wc, &Frame{
|
||||
Feature: Feature,
|
||||
Type: res.Type,
|
||||
Seq: ctx.seq,
|
||||
Flag: FlagComplete,
|
||||
Error: fmt.Sprintf("ERROR(%d): %s", res.Code, res.Reason),
|
||||
})
|
||||
return
|
||||
}
|
||||
if res.Data == nil {
|
||||
buf = OK
|
||||
goto __END
|
||||
}
|
||||
if marshal, ok = res.Data.(encoder); ok {
|
||||
buf, err = marshal.Marshal()
|
||||
goto __END
|
||||
}
|
||||
buf, err = serialize(res.Data)
|
||||
__END:
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
offset := 0
|
||||
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 {
|
||||
return
|
||||
}
|
||||
offset += chunkSize
|
||||
}
|
||||
err = writeFrame(ctx.wc, newFrame(res.Type, FlagComplete, ctx.seq, buf[offset:]))
|
||||
return
|
||||
}
|
||||
|
||||
func newContext(id int64, wc io.WriteCloser) *Context {
|
||||
return &Context{
|
||||
Id: id,
|
||||
wc: wc,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
PacketTypeCompleter byte = 0x01
|
||||
PacketTypeCommand = 0x02
|
||||
PacketTypeHandshake = 0x03
|
||||
)
|
||||
|
||||
const (
|
||||
FlagPortion = 0x00
|
||||
FlagComplete = 0x01
|
||||
)
|
||||
|
||||
type (
|
||||
Frame struct {
|
||||
Feature []byte
|
||||
Type byte `json:"type"`
|
||||
Flag byte `json:"flag"`
|
||||
Seq uint16 `json:"seq"`
|
||||
Data []byte `json:"data"`
|
||||
Error string `json:"error"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
)
|
||||
|
||||
func readFrame(r io.Reader) (frame *Frame, err error) {
|
||||
var (
|
||||
n int
|
||||
dataLength uint16
|
||||
errorLength uint16
|
||||
errBuf []byte
|
||||
)
|
||||
frame = &Frame{Feature: make([]byte, 3)}
|
||||
if _, err = io.ReadFull(r, frame.Feature); err != nil {
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(frame.Feature, Feature) {
|
||||
err = io.ErrUnexpectedEOF
|
||||
return
|
||||
}
|
||||
if err = binary.Read(r, binary.LittleEndian, &frame.Type); err != nil {
|
||||
return
|
||||
}
|
||||
if err = binary.Read(r, binary.LittleEndian, &frame.Flag); err != nil {
|
||||
return
|
||||
}
|
||||
if err = binary.Read(r, binary.LittleEndian, &frame.Seq); err != nil {
|
||||
return
|
||||
}
|
||||
if err = binary.Read(r, binary.LittleEndian, &frame.Timestamp); err != nil {
|
||||
return
|
||||
}
|
||||
if err = binary.Read(r, binary.LittleEndian, &dataLength); err != nil {
|
||||
return
|
||||
}
|
||||
if err = binary.Read(r, binary.LittleEndian, &errorLength); err != nil {
|
||||
return
|
||||
}
|
||||
if dataLength > 0 {
|
||||
frame.Data = make([]byte, dataLength)
|
||||
if n, err = io.ReadFull(r, frame.Data); err == nil {
|
||||
if n < int(dataLength) {
|
||||
err = io.ErrShortBuffer
|
||||
}
|
||||
}
|
||||
}
|
||||
if errorLength > 0 {
|
||||
errBuf = make([]byte, errorLength)
|
||||
if n, err = io.ReadFull(r, errBuf); err == nil {
|
||||
if n < int(dataLength) {
|
||||
err = io.ErrShortBuffer
|
||||
} else {
|
||||
frame.Error = string(errBuf)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func writeFrame(w io.Writer, frame *Frame) (err error) {
|
||||
var (
|
||||
n int
|
||||
dl int
|
||||
dataLength uint16
|
||||
errorLength uint16
|
||||
errBuf []byte
|
||||
)
|
||||
if _, err = w.Write(Feature); err != nil {
|
||||
return
|
||||
}
|
||||
if frame.Data != nil {
|
||||
dl = len(frame.Data)
|
||||
if dl > math.MaxUint16 {
|
||||
return io.ErrNoProgress
|
||||
}
|
||||
dataLength = uint16(dl)
|
||||
}
|
||||
if frame.Error != "" {
|
||||
errBuf = []byte(frame.Error)
|
||||
errorLength = uint16(len(errBuf))
|
||||
}
|
||||
if err = binary.Write(w, binary.LittleEndian, frame.Type); err != nil {
|
||||
return
|
||||
}
|
||||
if err = binary.Write(w, binary.LittleEndian, frame.Flag); err != nil {
|
||||
return
|
||||
}
|
||||
if err = binary.Write(w, binary.LittleEndian, frame.Seq); err != nil {
|
||||
return
|
||||
}
|
||||
if err = binary.Write(w, binary.LittleEndian, frame.Timestamp); err != nil {
|
||||
return
|
||||
}
|
||||
if err = binary.Write(w, binary.LittleEndian, dataLength); err != nil {
|
||||
return
|
||||
}
|
||||
if err = binary.Write(w, binary.LittleEndian, errorLength); err != nil {
|
||||
return
|
||||
}
|
||||
if dataLength > 0 {
|
||||
if n, err = w.Write(frame.Data); err == nil {
|
||||
if n < int(dataLength) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
}
|
||||
if errorLength > 0 {
|
||||
if n, err = w.Write(errBuf); err == nil {
|
||||
if n < int(errorLength) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newFrame(t, f byte, seq uint16, data []byte) *Frame {
|
||||
return &Frame{
|
||||
Feature: Feature,
|
||||
Type: t,
|
||||
Flag: f,
|
||||
Seq: seq,
|
||||
Data: data,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
name string
|
||||
path []string
|
||||
children []*Router
|
||||
command Command
|
||||
params []string
|
||||
}
|
||||
|
||||
func (r *Router) getChildren(name string) *Router {
|
||||
for _, child := range r.children {
|
||||
if child.name == name {
|
||||
return child
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) Completer(tokens ...string) []string {
|
||||
ss := make([]string, 0, 10)
|
||||
if len(tokens) == 0 {
|
||||
for _, child := range r.children {
|
||||
ss = append(ss, strings.Join(child.path, " "))
|
||||
}
|
||||
return ss
|
||||
}
|
||||
children := r.getChildren(tokens[0])
|
||||
if children == nil {
|
||||
token := tokens[0]
|
||||
for _, child := range r.children {
|
||||
if strings.HasPrefix(child.name, token) {
|
||||
ss = append(ss, strings.Join(child.path, " "))
|
||||
}
|
||||
}
|
||||
return ss
|
||||
}
|
||||
return children.Completer(tokens[1:]...)
|
||||
}
|
||||
|
||||
func (r *Router) Usage() string {
|
||||
if len(r.path) <= 0 {
|
||||
return ""
|
||||
}
|
||||
var (
|
||||
sb strings.Builder
|
||||
)
|
||||
sb.WriteString("Usage: ")
|
||||
sb.WriteString(strings.Join(r.path, " "))
|
||||
if len(r.params) > 0 {
|
||||
for _, s := range r.params {
|
||||
sb.WriteString(" {" + s + "}")
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (r *Router) Handle(path string, command Command) {
|
||||
var (
|
||||
pos int
|
||||
name string
|
||||
)
|
||||
if strings.HasSuffix(path, "/") {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
}
|
||||
if strings.HasPrefix(path, "/") {
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
}
|
||||
if path == "" {
|
||||
r.command = command
|
||||
return
|
||||
}
|
||||
if path[0] == ':' {
|
||||
ss := strings.Split(path, "/")
|
||||
for _, s := range ss {
|
||||
r.params = append(r.params, strings.TrimPrefix(s, ":"))
|
||||
}
|
||||
r.command = command
|
||||
return
|
||||
}
|
||||
if pos = strings.IndexByte(path, '/'); pos > -1 {
|
||||
name = path[:pos]
|
||||
path = path[pos:]
|
||||
} else {
|
||||
name = path
|
||||
path = ""
|
||||
}
|
||||
children := r.getChildren(name)
|
||||
if children == nil {
|
||||
children = newRouter(name)
|
||||
if len(r.path) == 0 {
|
||||
children.path = append(children.path, name)
|
||||
} else {
|
||||
children.path = append(children.path, r.path...)
|
||||
children.path = append(children.path, name)
|
||||
}
|
||||
r.children = append(r.children, children)
|
||||
}
|
||||
if children.command.Handle != nil {
|
||||
panic("a handle is already registered for path /" + strings.Join(children.path, "/"))
|
||||
}
|
||||
children.Handle(path, command)
|
||||
}
|
||||
|
||||
func (r *Router) Lookup(tokens []string) (router *Router, args []string, err error) {
|
||||
if len(tokens) > 0 {
|
||||
children := r.getChildren(tokens[0])
|
||||
if children != nil {
|
||||
return children.Lookup(tokens[1:])
|
||||
}
|
||||
}
|
||||
if r.command.Handle == nil {
|
||||
err = ErrNotFound
|
||||
return
|
||||
}
|
||||
router = r
|
||||
args = tokens
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Router) String() string {
|
||||
var (
|
||||
sb strings.Builder
|
||||
width int
|
||||
maxWidth int
|
||||
walkFunc func(router *Router) []commander
|
||||
)
|
||||
walkFunc = func(router *Router) []commander {
|
||||
vs := make([]commander, 0, 5)
|
||||
if router.command.Handle != nil {
|
||||
vs = append(vs, commander{
|
||||
Name: router.name,
|
||||
Path: strings.Join(router.path, " "),
|
||||
Description: router.command.Description,
|
||||
})
|
||||
} else {
|
||||
if len(router.children) > 0 {
|
||||
for _, child := range router.children {
|
||||
vs = append(vs, walkFunc(child)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return vs
|
||||
}
|
||||
vs := walkFunc(r)
|
||||
for _, v := range vs {
|
||||
width = runewidth.StringWidth(v.Path)
|
||||
if width > maxWidth {
|
||||
maxWidth = width
|
||||
}
|
||||
}
|
||||
for _, v := range vs {
|
||||
sb.WriteString(fmt.Sprintf("%-"+strconv.Itoa(maxWidth+4)+"s %s\n", v.Path, v.Description))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func newRouter(name string) *Router {
|
||||
return &Router{
|
||||
name: name,
|
||||
path: make([]string, 0, 4),
|
||||
params: make([]string, 0, 4),
|
||||
children: make([]*Router, 0, 10),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,250 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.nspix.com/golang/kos/util/pool"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func isNormalKind(kind reflect.Kind) bool {
|
||||
normalKinds := []reflect.Kind{
|
||||
reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int,
|
||||
reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint,
|
||||
reflect.Float32, reflect.Float64,
|
||||
reflect.String,
|
||||
}
|
||||
for _, k := range normalKinds {
|
||||
if k == kind {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func serializeMap(val map[any]any) ([]byte, error) {
|
||||
var (
|
||||
canFormat bool
|
||||
width int
|
||||
maxWidth int
|
||||
)
|
||||
canFormat = true
|
||||
for k, v := range val {
|
||||
if !isNormalKind(reflect.Indirect(reflect.ValueOf(k)).Kind()) || !isNormalKind(reflect.Indirect(reflect.ValueOf(v)).Kind()) {
|
||||
canFormat = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !canFormat {
|
||||
return json.MarshalIndent(val, "", "\t")
|
||||
}
|
||||
ms := make(map[string]string)
|
||||
for k, v := range val {
|
||||
sk := fmt.Sprint(k)
|
||||
ms[sk] = fmt.Sprint(v)
|
||||
width = runewidth.StringWidth(sk)
|
||||
if width > maxWidth {
|
||||
maxWidth = width
|
||||
}
|
||||
}
|
||||
buffer := pool.GetBuffer()
|
||||
defer pool.PutBuffer(buffer)
|
||||
for k, v := range ms {
|
||||
buffer.WriteString(fmt.Sprintf("%-"+strconv.Itoa(maxWidth+4)+"s %s\n", k, v))
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func printBorder(w *bytes.Buffer, ws []int) {
|
||||
for _, l := range ws {
|
||||
w.WriteString("+")
|
||||
w.WriteString(strings.Repeat("-", l+2))
|
||||
}
|
||||
w.WriteString("+\n")
|
||||
}
|
||||
|
||||
func toString(v any) string {
|
||||
switch t := v.(type) {
|
||||
case float32, float64:
|
||||
return fmt.Sprintf("%.2f", t)
|
||||
case time.Time:
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
default:
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
}
|
||||
|
||||
func printArray(vals [][]any) (buf []byte) {
|
||||
var (
|
||||
cell string
|
||||
str string
|
||||
widths []int
|
||||
maxLength int
|
||||
width int
|
||||
rows [][]string
|
||||
)
|
||||
rows = make([][]string, 0, len(vals))
|
||||
for _, value := range vals {
|
||||
if len(value) > maxLength {
|
||||
maxLength = len(value)
|
||||
}
|
||||
}
|
||||
widths = make([]int, maxLength)
|
||||
for _, vs := range vals {
|
||||
rl := len(vs)
|
||||
row := make([]string, rl)
|
||||
for i, val := range vs {
|
||||
str = toString(val)
|
||||
if rl > 1 {
|
||||
width = runewidth.StringWidth(str)
|
||||
if width > widths[i] {
|
||||
widths[i] = width
|
||||
}
|
||||
}
|
||||
row[i] = str
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
buffer := pool.GetBuffer()
|
||||
defer pool.PutBuffer(buffer)
|
||||
printBorder(buffer, widths)
|
||||
for index, row := range rows {
|
||||
size := len(row)
|
||||
for i, w := range widths {
|
||||
cell = ""
|
||||
buffer.WriteString("|")
|
||||
if size > i {
|
||||
cell = row[i]
|
||||
}
|
||||
buffer.WriteString(" ")
|
||||
buffer.WriteString(cell)
|
||||
cl := runewidth.StringWidth(cell)
|
||||
if w > cl {
|
||||
buffer.WriteString(strings.Repeat(" ", w-cl))
|
||||
}
|
||||
buffer.WriteString(" ")
|
||||
}
|
||||
buffer.WriteString("|\n")
|
||||
if index == 0 {
|
||||
printBorder(buffer, widths)
|
||||
}
|
||||
}
|
||||
printBorder(buffer, widths)
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func serializeArray(val []any) (buf []byte, err error) {
|
||||
var (
|
||||
ok bool
|
||||
vs [][]any
|
||||
normalFormat bool
|
||||
isArrayElement bool
|
||||
isStructElement bool
|
||||
columnName string
|
||||
)
|
||||
normalFormat = true
|
||||
for _, row := range val {
|
||||
kind := reflect.Indirect(reflect.ValueOf(row)).Kind()
|
||||
if !isNormalKind(kind) {
|
||||
normalFormat = false
|
||||
}
|
||||
if kind == reflect.Array || kind == reflect.Slice {
|
||||
isArrayElement = true
|
||||
}
|
||||
if kind == reflect.Struct {
|
||||
isStructElement = true
|
||||
}
|
||||
}
|
||||
if normalFormat {
|
||||
goto __END
|
||||
}
|
||||
if isArrayElement {
|
||||
vs = make([][]any, 0, len(val))
|
||||
for _, v := range val {
|
||||
rv := reflect.Indirect(reflect.ValueOf(v))
|
||||
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 {
|
||||
row = append(row, rv.Index(i).Interface())
|
||||
} else {
|
||||
goto __END
|
||||
}
|
||||
}
|
||||
vs = append(vs, row)
|
||||
} else {
|
||||
goto __END
|
||||
}
|
||||
}
|
||||
}
|
||||
if isStructElement {
|
||||
vs = make([][]any, 0, len(val))
|
||||
for i, v := range val {
|
||||
rv := reflect.Indirect(reflect.ValueOf(v))
|
||||
if rv.Kind() == reflect.Struct {
|
||||
if i == 0 {
|
||||
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 {
|
||||
columnName = strings.ToUpper(rv.Type().Field(j).Name)
|
||||
}
|
||||
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())
|
||||
}
|
||||
vs = append(vs, row)
|
||||
} else {
|
||||
goto __END
|
||||
}
|
||||
}
|
||||
}
|
||||
buf = printArray(vs)
|
||||
return
|
||||
__END:
|
||||
return json.MarshalIndent(val, "", "\t")
|
||||
}
|
||||
|
||||
func serialize(val any) (buf []byte, err error) {
|
||||
var (
|
||||
refVal reflect.Value
|
||||
)
|
||||
refVal = reflect.Indirect(reflect.ValueOf(val))
|
||||
switch refVal.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
buf = []byte(strconv.FormatInt(refVal.Int(), 10))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
buf = []byte(strconv.FormatFloat(refVal.Float(), 'f', -1, 64))
|
||||
case reflect.String:
|
||||
buf = []byte(refVal.String())
|
||||
case reflect.Slice, reflect.Array:
|
||||
if refVal.Type().Elem().Kind() == reflect.Uint8 {
|
||||
buf = refVal.Bytes()
|
||||
} else {
|
||||
as := make([]any, 0, refVal.Len())
|
||||
for i := 0; i < refVal.Len(); i++ {
|
||||
as = append(as, refVal.Index(i).Interface())
|
||||
}
|
||||
buf, err = serializeArray(as)
|
||||
}
|
||||
case reflect.Map:
|
||||
ms := make(map[any]any)
|
||||
keys := refVal.MapKeys()
|
||||
for _, key := range keys {
|
||||
ms[key.Interface()] = refVal.MapIndex(key).Interface()
|
||||
}
|
||||
buf, err = serializeMap(ms)
|
||||
default:
|
||||
buf, err = json.MarshalIndent(refVal.Interface(), "", "\t")
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.nspix.com/golang/kos/util/env"
|
||||
"github.com/sourcegraph/conc"
|
||||
"net"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ctxPool sync.Pool
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
ctx context.Context
|
||||
sequence int64
|
||||
ctxMap sync.Map
|
||||
waitGroup conc.WaitGroup
|
||||
middleware []Middleware
|
||||
router *Router
|
||||
l net.Listener
|
||||
}
|
||||
|
||||
func (svr *Server) applyContext() *Context {
|
||||
if v := ctxPool.Get(); v != nil {
|
||||
if ctx, ok := v.(*Context); ok {
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
return &Context{}
|
||||
}
|
||||
|
||||
func (svr *Server) releaseContext(ctx *Context) {
|
||||
ctxPool.Put(ctx)
|
||||
}
|
||||
|
||||
func (svr *Server) handle(ctx *Context, frame *Frame) {
|
||||
var (
|
||||
err error
|
||||
params map[string]string
|
||||
tokens []string
|
||||
args []string
|
||||
r *Router
|
||||
)
|
||||
cmd := string(frame.Data)
|
||||
tokens = strings.Fields(cmd)
|
||||
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))
|
||||
} else {
|
||||
err = ctx.Error(errExecuteFailed, err.Error())
|
||||
}
|
||||
} else {
|
||||
if len(r.params) > len(args) {
|
||||
err = ctx.Error(errExecuteFailed, r.Usage())
|
||||
return
|
||||
}
|
||||
if len(r.params) > 0 {
|
||||
params = make(map[string]string)
|
||||
for i, s := range r.params {
|
||||
params[s] = args[i]
|
||||
}
|
||||
}
|
||||
ctx.setArgs(args)
|
||||
ctx.setParam(params)
|
||||
err = r.command.Handle(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (svr *Server) process(conn net.Conn) {
|
||||
var (
|
||||
err error
|
||||
ctx *Context
|
||||
frame *Frame
|
||||
)
|
||||
ctx = svr.applyContext()
|
||||
ctx.reset(atomic.AddInt64(&svr.sequence, 1), conn)
|
||||
svr.ctxMap.Store(ctx.Id, ctx)
|
||||
defer func() {
|
||||
_ = conn.Close()
|
||||
svr.ctxMap.Delete(ctx.Id)
|
||||
svr.releaseContext(ctx)
|
||||
}()
|
||||
for {
|
||||
if frame, err = readFrame(conn); err != nil {
|
||||
break
|
||||
}
|
||||
//reset frame
|
||||
ctx.seq = frame.Seq
|
||||
switch frame.Type {
|
||||
case PacketTypeHandshake:
|
||||
if err = ctx.send(responsePayload{
|
||||
Type: PacketTypeHandshake,
|
||||
Data: &Info{
|
||||
ID: ctx.Id,
|
||||
Name: env.Get("VOX_NAME", ""),
|
||||
Version: env.Get("VOX_VERSION", ""),
|
||||
OS: runtime.GOOS,
|
||||
ServerTime: time.Now(),
|
||||
RemoteAddr: conn.RemoteAddr().String(),
|
||||
},
|
||||
}); err != nil {
|
||||
break
|
||||
}
|
||||
case PacketTypeCompleter:
|
||||
if err = ctx.send(responsePayload{
|
||||
Type: PacketTypeCompleter,
|
||||
Data: svr.router.Completer(strings.Fields(string(frame.Data))...),
|
||||
}); err != nil {
|
||||
break
|
||||
}
|
||||
case PacketTypeCommand:
|
||||
svr.handle(ctx, frame)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (svr *Server) serve() {
|
||||
for {
|
||||
conn, err := svr.l.Accept()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
svr.waitGroup.Go(func() {
|
||||
svr.process(conn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (svr *Server) wrapHandle(pathname, desc string, cb HandleFunc, middleware ...Middleware) Command {
|
||||
h := func(ctx *Context) (err error) {
|
||||
for i := len(svr.middleware) - 1; i >= 0; i-- {
|
||||
cb = svr.middleware[i](cb)
|
||||
}
|
||||
for i := len(middleware) - 1; i >= 0; i-- {
|
||||
cb = middleware[i](cb)
|
||||
}
|
||||
return cb(ctx)
|
||||
}
|
||||
if desc == "" {
|
||||
desc = strings.Join(strings.Split(strings.TrimPrefix(pathname, "/"), "/"), " ")
|
||||
}
|
||||
return Command{
|
||||
Path: pathname,
|
||||
Handle: h,
|
||||
Description: desc,
|
||||
}
|
||||
}
|
||||
|
||||
func (svr *Server) Use(middleware ...Middleware) {
|
||||
svr.middleware = append(svr.middleware, middleware...)
|
||||
}
|
||||
|
||||
func (svr *Server) Group(prefix string, commands []Command, middleware ...Middleware) {
|
||||
for _, cmd := range commands {
|
||||
svr.Handle(path.Join(prefix, cmd.Path), cmd.Description, cmd.Handle, middleware...)
|
||||
}
|
||||
}
|
||||
|
||||
func (svr *Server) Handle(pathname string, desc string, cb HandleFunc, middleware ...Middleware) {
|
||||
svr.router.Handle(pathname, svr.wrapHandle(pathname, desc, cb, middleware...))
|
||||
}
|
||||
|
||||
func (svr *Server) Serve(l net.Listener) (err error) {
|
||||
svr.l = l
|
||||
svr.Handle("/help", "Display help information", func(ctx *Context) (err error) {
|
||||
return ctx.Success(svr.router.String())
|
||||
})
|
||||
svr.serve()
|
||||
return
|
||||
}
|
||||
|
||||
func (svr *Server) Shutdown() (err error) {
|
||||
err = svr.l.Close()
|
||||
svr.ctxMap.Range(func(key, value any) bool {
|
||||
if ctx, ok := value.(*Context); ok {
|
||||
err = ctx.Close()
|
||||
}
|
||||
return true
|
||||
})
|
||||
svr.waitGroup.Wait()
|
||||
return
|
||||
}
|
||||
|
||||
func New(ctx context.Context) *Server {
|
||||
return &Server{
|
||||
ctx: ctx,
|
||||
router: newRouter(""),
|
||||
middleware: make([]Middleware, 0, 10),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package cli
|
||||
|
||||
import "time"
|
||||
|
||||
type Param struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
type Params []Param
|
||||
|
||||
type HandleFunc func(ctx *Context) (err error)
|
||||
|
||||
type Middleware func(next HandleFunc) HandleFunc
|
||||
|
||||
type responsePayload struct {
|
||||
Type uint8 `json:"-"`
|
||||
Code int `json:"code"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
ID int64 `json:"id"`
|
||||
OS string `json:"os"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
ServerTime time.Time `json:"server_time"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
Path string
|
||||
Handle HandleFunc
|
||||
Description string
|
||||
}
|
||||
|
||||
type commander struct {
|
||||
Name string
|
||||
Path string
|
||||
Description string
|
||||
}
|
||||
|
||||
type encoder interface {
|
||||
Marshal() ([]byte, error)
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package entry
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
buf []byte
|
||||
conn net.Conn
|
||||
exitFlag int32
|
||||
state *State
|
||||
}
|
||||
|
||||
func (c *Conn) Read(b []byte) (n int, err error) {
|
||||
var m int
|
||||
if len(c.buf) > 0 {
|
||||
if len(b) >= len(c.buf) {
|
||||
m = copy(b[:], c.buf[:])
|
||||
c.buf = c.buf[m:]
|
||||
}
|
||||
}
|
||||
n, err = c.conn.Read(b[m:])
|
||||
n += m
|
||||
atomic.AddInt64(&c.state.Traffic.In, 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))
|
||||
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)
|
||||
return c.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) LocalAddr() net.Addr {
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *Conn) RemoteAddr() net.Addr {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *Conn) SetDeadline(t time.Time) error {
|
||||
return c.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *Conn) SetReadDeadline(t time.Time) error {
|
||||
return c.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *Conn) SetWriteDeadline(t time.Time) error {
|
||||
return c.conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func wrapConn(c net.Conn, state *State, buf []byte) net.Conn {
|
||||
conn := &Conn{conn: c, buf: buf, state: state}
|
||||
if buf != nil {
|
||||
conn.buf = make([]byte, len(buf))
|
||||
copy(conn.buf, buf)
|
||||
}
|
||||
return conn
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
package entry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/sourcegraph/conc"
|
||||
"io"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
minFeatureLength = 3
|
||||
)
|
||||
|
||||
var (
|
||||
ErrShortFeature = errors.New("short feature")
|
||||
ErrInvalidListener = errors.New("invalid listener")
|
||||
)
|
||||
|
||||
type (
|
||||
Feature []byte
|
||||
|
||||
listenerEntity struct {
|
||||
feature Feature
|
||||
listener *Listener
|
||||
}
|
||||
|
||||
Gateway struct {
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelCauseFunc
|
||||
l net.Listener
|
||||
ch chan net.Conn
|
||||
address string
|
||||
state *State
|
||||
waitGroup conc.WaitGroup
|
||||
listeners []*listenerEntity
|
||||
exitFlag int32
|
||||
}
|
||||
)
|
||||
|
||||
func (gw *Gateway) handle(conn net.Conn) {
|
||||
var (
|
||||
n int
|
||||
err error
|
||||
successed int32
|
||||
feature = make([]byte, minFeatureLength)
|
||||
)
|
||||
atomic.AddInt32(&gw.state.Concurrency, 1)
|
||||
defer func() {
|
||||
if atomic.LoadInt32(&successed) != 1 {
|
||||
atomic.AddInt32(&gw.state.Concurrency, -1)
|
||||
atomic.AddInt64(&gw.state.Request.Discarded, 1)
|
||||
_ = conn.Close()
|
||||
}
|
||||
}()
|
||||
//set deadline
|
||||
if err = conn.SetReadDeadline(time.Now().Add(time.Second * 30)); err != nil {
|
||||
return
|
||||
}
|
||||
//read feature
|
||||
if n, err = io.ReadFull(conn, feature); err != nil {
|
||||
return
|
||||
}
|
||||
//reset deadline
|
||||
if err = conn.SetReadDeadline(time.Time{}); err != nil {
|
||||
return
|
||||
}
|
||||
for _, l := range gw.listeners {
|
||||
if bytes.Compare(feature[:n], l.feature[:n]) == 0 {
|
||||
atomic.StoreInt32(&successed, 1)
|
||||
l.listener.Receive(wrapConn(conn, gw.state, feature[:n]))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gw *Gateway) accept() {
|
||||
atomic.StoreInt32(&gw.state.Accepting, 1)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&gw.state.Accepting, 0)
|
||||
}()
|
||||
for {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gw *Gateway) worker() {
|
||||
atomic.StoreInt32(&gw.state.Processing, 1)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&gw.state.Processing, 0)
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-gw.ctx.Done():
|
||||
return
|
||||
case conn, ok := <-gw.ch:
|
||||
if ok {
|
||||
gw.handle(conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gw *Gateway) Bind(feature Feature, listener net.Listener) (err error) {
|
||||
var (
|
||||
ok bool
|
||||
ls *Listener
|
||||
)
|
||||
if len(feature) < minFeatureLength {
|
||||
return ErrShortFeature
|
||||
}
|
||||
if ls, ok = listener.(*Listener); !ok {
|
||||
return ErrInvalidListener
|
||||
}
|
||||
for _, l := range gw.listeners {
|
||||
if bytes.Compare(l.feature, feature) == 0 {
|
||||
l.listener = ls
|
||||
return
|
||||
}
|
||||
}
|
||||
gw.listeners = append(gw.listeners, &listenerEntity{
|
||||
feature: feature,
|
||||
listener: ls,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (gw *Gateway) Apply(feature ...Feature) (listener net.Listener, err error) {
|
||||
listener = newListener(gw.l.Addr())
|
||||
for _, code := range feature {
|
||||
if len(code) < minFeatureLength {
|
||||
continue
|
||||
}
|
||||
err = gw.Bind(code, listener)
|
||||
}
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
func (gw *Gateway) Release(feature Feature) {
|
||||
for i, l := range gw.listeners {
|
||||
if bytes.Compare(l.feature, feature) == 0 {
|
||||
gw.listeners = append(gw.listeners[:i], gw.listeners[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gw *Gateway) State() *State {
|
||||
return gw.state
|
||||
}
|
||||
|
||||
func (gw *Gateway) Start(ctx context.Context) (err error) {
|
||||
gw.ctx, gw.cancelFunc = context.WithCancelCause(ctx)
|
||||
if gw.l, err = net.Listen("tcp", gw.address); err != nil {
|
||||
return
|
||||
}
|
||||
gw.waitGroup.Go(gw.worker)
|
||||
gw.waitGroup.Go(gw.accept)
|
||||
return
|
||||
}
|
||||
|
||||
func (gw *Gateway) Stop() (err error) {
|
||||
if !atomic.CompareAndSwapInt32(&gw.exitFlag, 0, 1) {
|
||||
return
|
||||
}
|
||||
gw.cancelFunc(io.ErrClosedPipe)
|
||||
err = gw.l.Close()
|
||||
gw.waitGroup.Wait()
|
||||
close(gw.ch)
|
||||
return
|
||||
}
|
||||
|
||||
func New(address string) *Gateway {
|
||||
gw := &Gateway{
|
||||
address: address,
|
||||
state: &State{},
|
||||
ch: make(chan net.Conn, 10),
|
||||
}
|
||||
return gw
|
||||
}
|
|
@ -0,0 +1,271 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
// Binder is the interface that wraps the Bind method.
|
||||
Binder interface {
|
||||
Bind(i interface{}, req *http.Request) error
|
||||
}
|
||||
|
||||
// DefaultBinder is the default implementation of the Binder interface.
|
||||
DefaultBinder struct{}
|
||||
|
||||
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
|
||||
BindUnmarshaler interface {
|
||||
// UnmarshalParam decodes and assigns a value from an form or query param.
|
||||
UnmarshalParam(param string) error
|
||||
}
|
||||
)
|
||||
|
||||
// Bind implements the `Binder#Bind` function.
|
||||
func (b *DefaultBinder) Bind(i any, req *http.Request) (err error) {
|
||||
if req.ContentLength == 0 {
|
||||
if req.Method == http.MethodGet || req.Method == http.MethodDelete {
|
||||
if err = b.bindData(i, req.URL.Query(), "query"); err != nil {
|
||||
return err
|
||||
}
|
||||
return
|
||||
}
|
||||
return errors.New("request body can't be empty")
|
||||
}
|
||||
ctype := req.Header.Get("Content-Type")
|
||||
switch {
|
||||
case strings.HasPrefix(ctype, "application/json"):
|
||||
if err = json.NewDecoder(req.Body).Decode(i); err != nil {
|
||||
if ute, ok := err.(*json.UnmarshalTypeError); ok {
|
||||
return fmt.Errorf("unmarshal type error: expected=%v, got=%v, field=%v, offset=%v", ute.Type, ute.Value, ute.Field, ute.Offset)
|
||||
} else if se, ok := err.(*json.SyntaxError); ok {
|
||||
return fmt.Errorf("syntax error: offset=%v, error=%v", se.Offset, se.Error())
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case strings.HasPrefix(ctype, "application/xml"), strings.HasPrefix(ctype, "text/xml"):
|
||||
if err = xml.NewDecoder(req.Body).Decode(i); err != nil {
|
||||
if ute, ok := err.(*xml.UnsupportedTypeError); ok {
|
||||
return fmt.Errorf("unsupported type error: type=%v, error=%v", ute.Type, ute.Error())
|
||||
} else if se, ok := err.(*xml.SyntaxError); ok {
|
||||
return fmt.Errorf("syntax error: line=%v, error=%v", se.Line, se.Error())
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case strings.HasPrefix(ctype, "application/x-www-form-urlencoded"), strings.HasPrefix(ctype, "multipart/form-data"):
|
||||
if strings.HasPrefix(req.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
if err := req.ParseMultipartForm(32 << 20); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = b.bindData(i, req.Form, "form"); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported formatter")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag string) error {
|
||||
typ := reflect.TypeOf(ptr).Elem()
|
||||
val := reflect.ValueOf(ptr).Elem()
|
||||
if typ.Kind() != reflect.Struct {
|
||||
return errors.New("binding element must be a struct")
|
||||
}
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
typeField := typ.Field(i)
|
||||
structField := val.Field(i)
|
||||
if !structField.CanSet() {
|
||||
continue
|
||||
}
|
||||
structFieldKind := structField.Kind()
|
||||
inputFieldName := typeField.Tag.Get(tag)
|
||||
if inputFieldName == "" {
|
||||
inputFieldName = typeField.Name
|
||||
// If tag is nil, we inspect if the field is a struct.
|
||||
if _, ok := bindUnmarshaler(structField); !ok && structFieldKind == reflect.Struct {
|
||||
if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
inputValue, exists := data[inputFieldName]
|
||||
if !exists {
|
||||
// Go json.Unmarshal supports case insensitive binding. However the
|
||||
// url params are bound case sensitive which is inconsistent. To
|
||||
// fix this we must check all of the map values in a
|
||||
// case-insensitive search.
|
||||
inputFieldName = strings.ToLower(inputFieldName)
|
||||
for k, v := range data {
|
||||
if strings.ToLower(k) == inputFieldName {
|
||||
inputValue = v
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
// Call this first, in case we're dealing with an alias to an array type
|
||||
if ok, err := unmarshalField(typeField.Type.Kind(), inputValue[0], structField); ok {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
numElems := len(inputValue)
|
||||
if structFieldKind == reflect.Slice && numElems > 0 {
|
||||
sliceOf := structField.Type().Elem().Kind()
|
||||
slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
|
||||
for j := 0; j < numElems; j++ {
|
||||
if err := setWithProperType(sliceOf, inputValue[j], slice.Index(j)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
val.Field(i).Set(slice)
|
||||
} else if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
|
||||
return err
|
||||
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
|
||||
// But also call it here, in case we're dealing with an array of BindUnmarshalers
|
||||
if ok, err := unmarshalField(valueKind, val, structField); ok {
|
||||
return err
|
||||
}
|
||||
switch valueKind {
|
||||
case reflect.Ptr:
|
||||
return setWithProperType(structField.Elem().Kind(), val, structField.Elem())
|
||||
case reflect.Int:
|
||||
return setIntField(val, 0, structField)
|
||||
case reflect.Int8:
|
||||
return setIntField(val, 8, structField)
|
||||
case reflect.Int16:
|
||||
return setIntField(val, 16, structField)
|
||||
case reflect.Int32:
|
||||
return setIntField(val, 32, structField)
|
||||
case reflect.Int64:
|
||||
return setIntField(val, 64, structField)
|
||||
case reflect.Uint:
|
||||
return setUintField(val, 0, structField)
|
||||
case reflect.Uint8:
|
||||
return setUintField(val, 8, structField)
|
||||
case reflect.Uint16:
|
||||
return setUintField(val, 16, structField)
|
||||
case reflect.Uint32:
|
||||
return setUintField(val, 32, structField)
|
||||
case reflect.Uint64:
|
||||
return setUintField(val, 64, structField)
|
||||
case reflect.Bool:
|
||||
return setBoolField(val, structField)
|
||||
case reflect.Float32:
|
||||
return setFloatField(val, 32, structField)
|
||||
case reflect.Float64:
|
||||
return setFloatField(val, 64, structField)
|
||||
case reflect.String:
|
||||
structField.SetString(val)
|
||||
default:
|
||||
return errors.New("unknown type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalField(valueKind reflect.Kind, val string, field reflect.Value) (bool, error) {
|
||||
switch valueKind {
|
||||
case reflect.Ptr:
|
||||
return unmarshalFieldPtr(val, field)
|
||||
default:
|
||||
return unmarshalFieldNonPtr(val, field)
|
||||
}
|
||||
}
|
||||
|
||||
// bindUnmarshaler attempts to unmarshal a reflect.Value into a BindUnmarshaler
|
||||
func bindUnmarshaler(field reflect.Value) (BindUnmarshaler, bool) {
|
||||
ptr := reflect.New(field.Type())
|
||||
if ptr.CanInterface() {
|
||||
iface := ptr.Interface()
|
||||
if unmarshaler, ok := iface.(BindUnmarshaler); ok {
|
||||
return unmarshaler, ok
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func unmarshalFieldNonPtr(value string, field reflect.Value) (bool, error) {
|
||||
if unmarshaler, ok := bindUnmarshaler(field); ok {
|
||||
err := unmarshaler.UnmarshalParam(value)
|
||||
field.Set(reflect.ValueOf(unmarshaler).Elem())
|
||||
return true, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func unmarshalFieldPtr(value string, field reflect.Value) (bool, error) {
|
||||
if field.IsNil() {
|
||||
// Initialize the pointer to a nil value
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
}
|
||||
return unmarshalFieldNonPtr(value, field.Elem())
|
||||
}
|
||||
|
||||
func setIntField(value string, bitSize int, field reflect.Value) error {
|
||||
if value == "" {
|
||||
value = "0"
|
||||
}
|
||||
intVal, err := strconv.ParseInt(value, 10, bitSize)
|
||||
if err == nil {
|
||||
field.SetInt(intVal)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func setUintField(value string, bitSize int, field reflect.Value) error {
|
||||
if value == "" {
|
||||
value = "0"
|
||||
}
|
||||
uintVal, err := strconv.ParseUint(value, 10, bitSize)
|
||||
if err == nil {
|
||||
field.SetUint(uintVal)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func setBoolField(value string, field reflect.Value) error {
|
||||
if value == "" {
|
||||
value = "false"
|
||||
}
|
||||
boolVal, err := strconv.ParseBool(value)
|
||||
if err == nil {
|
||||
field.SetBool(boolVal)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func setFloatField(value string, bitSize int, field reflect.Value) error {
|
||||
if value == "" {
|
||||
value = "0.0"
|
||||
}
|
||||
floatVal, err := strconv.ParseFloat(value, bitSize)
|
||||
if err == nil {
|
||||
field.SetFloat(floatVal)
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultBinder = &DefaultBinder{}
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
ctx context.Context
|
||||
req *http.Request
|
||||
res http.ResponseWriter
|
||||
params map[string]string
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (ctx *Context) reset(req *http.Request, res http.ResponseWriter, ps map[string]string) {
|
||||
ctx.statusCode = http.StatusOK
|
||||
ctx.req, ctx.res, ctx.params = req, res, ps
|
||||
}
|
||||
|
||||
func (ctx *Context) Request() *http.Request {
|
||||
return ctx.req
|
||||
}
|
||||
|
||||
func (ctx *Context) Response() http.ResponseWriter {
|
||||
return ctx.res
|
||||
}
|
||||
|
||||
func (ctx *Context) Context() context.Context {
|
||||
if ctx.Request().Context() != nil {
|
||||
return ctx.Request().Context()
|
||||
}
|
||||
return ctx.ctx
|
||||
}
|
||||
|
||||
func (ctx *Context) Bind(v any) (err error) {
|
||||
return defaultBinder.Bind(v, ctx.Request())
|
||||
}
|
||||
|
||||
func (ctx *Context) Query(k string) string {
|
||||
return ctx.Request().FormValue(k)
|
||||
}
|
||||
|
||||
func (ctx *Context) Param(k string) string {
|
||||
var (
|
||||
ok bool
|
||||
v string
|
||||
)
|
||||
if v, ok = ctx.params[k]; ok {
|
||||
return v
|
||||
}
|
||||
return ctx.Request().FormValue(k)
|
||||
}
|
||||
|
||||
func (ctx *Context) send(res responsePayload) (err error) {
|
||||
ctx.Response().Header().Set("Content-Type", "application/json")
|
||||
encoder := json.NewEncoder(ctx.Response())
|
||||
if strings.HasPrefix(ctx.Request().Header.Get("User-Agent"), "curl") {
|
||||
encoder.SetIndent("", "\t")
|
||||
}
|
||||
return encoder.Encode(res)
|
||||
}
|
||||
|
||||
func (ctx *Context) Success(v any) (err error) {
|
||||
return ctx.send(responsePayload{Data: v})
|
||||
}
|
||||
|
||||
func (ctx *Context) Status(code int) {
|
||||
ctx.statusCode = code
|
||||
}
|
||||
|
||||
func (ctx *Context) Error(code int, reason string) (err error) {
|
||||
return ctx.send(responsePayload{Code: code, Reason: reason})
|
||||
}
|
||||
|
||||
func (ctx *Context) Redirect(url string, code int) {
|
||||
if code != http.StatusFound && code != http.StatusMovedPermanently {
|
||||
code = http.StatusMovedPermanently
|
||||
}
|
||||
http.Redirect(ctx.Response(), ctx.Request(), url, code)
|
||||
}
|
||||
|
||||
func (ctx *Context) SetCookie(cookie *http.Cookie) {
|
||||
http.SetCookie(ctx.Response(), cookie)
|
||||
}
|
||||
|
||||
func (ctx *Context) SendFile(filename string) (err error) {
|
||||
var (
|
||||
fi os.FileInfo
|
||||
fp *os.File
|
||||
)
|
||||
if fi, err = os.Stat(filename); err == nil {
|
||||
if fp, err = os.Open(filename); err == nil {
|
||||
http.ServeContent(ctx.Response(), ctx.Request(), path.Base(filename), fi.ModTime(), fp)
|
||||
err = fp.Close()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type (
|
||||
NotFound struct {
|
||||
}
|
||||
NotAllowed struct {
|
||||
}
|
||||
)
|
||||
|
||||
func (n NotFound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(writer).Encode(responsePayload{
|
||||
Code: http.StatusNotFound,
|
||||
Reason: fmt.Sprintf("requested URL %s was not found on this server", request.URL.Path),
|
||||
})
|
||||
}
|
||||
|
||||
func (n NotAllowed) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.WriteHeader(http.StatusMethodNotAllowed)
|
||||
json.NewEncoder(writer).Encode(responsePayload{
|
||||
Code: http.StatusMethodNotAllowed,
|
||||
Reason: fmt.Sprintf("%s URL %s was not allow on this server", request.Method, request.URL.Path),
|
||||
})
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
const (
|
||||
MethodGet = http.MethodGet
|
||||
MethodHead = http.MethodHead
|
||||
MethodPost = http.MethodPost
|
||||
MethodPut = http.MethodPut
|
||||
MethodPatch = http.MethodPatch // RFC 5789
|
||||
MethodDelete = http.MethodDelete
|
||||
MethodConnect = http.MethodConnect
|
||||
MethodOptions = http.MethodOptions
|
||||
MethodTrace = http.MethodTrace
|
||||
)
|
|
@ -0,0 +1,145 @@
|
|||
package router
|
||||
|
||||
// CleanPath is the URL version of path.Clean, it returns a canonical URL path
|
||||
// for p, eliminating . and .. elements.
|
||||
//
|
||||
// The following rules are applied iteratively until no further processing can
|
||||
// be done:
|
||||
// 1. Replace multiple slashes with a single slash.
|
||||
// 2. Eliminate each . path name element (the current directory).
|
||||
// 3. Eliminate each inner .. path name element (the parent directory)
|
||||
// along with the non-.. element that precedes it.
|
||||
// 4. Eliminate .. elements that begin a rooted path:
|
||||
// that is, replace "/.." by "/" at the beginning of a path.
|
||||
//
|
||||
// If the result of this process is an empty string, "/" is returned
|
||||
func CleanPath(p string) string {
|
||||
const stackBufSize = 128
|
||||
|
||||
// Turn empty string into "/"
|
||||
if p == "" {
|
||||
return "/"
|
||||
}
|
||||
|
||||
// Reasonably sized buffer on stack to avoid allocations in the common case.
|
||||
// If a larger buffer is required, it gets allocated dynamically.
|
||||
buf := make([]byte, 0, stackBufSize)
|
||||
|
||||
n := len(p)
|
||||
|
||||
// Invariants:
|
||||
// reading from path; r is index of next byte to process.
|
||||
// writing to buf; w is index of next byte to write.
|
||||
|
||||
// path must start with '/'
|
||||
r := 1
|
||||
w := 1
|
||||
|
||||
if p[0] != '/' {
|
||||
r = 0
|
||||
|
||||
if n+1 > stackBufSize {
|
||||
buf = make([]byte, n+1)
|
||||
} else {
|
||||
buf = buf[:n+1]
|
||||
}
|
||||
buf[0] = '/'
|
||||
}
|
||||
|
||||
trailing := n > 1 && p[n-1] == '/'
|
||||
|
||||
// A bit more clunky without a 'lazybuf' like the path package, but the loop
|
||||
// gets completely inlined (bufApp calls).
|
||||
// So in contrast to the path package this loop has no expensive function
|
||||
// calls (except make, if needed).
|
||||
|
||||
for r < n {
|
||||
switch {
|
||||
case p[r] == '/':
|
||||
// empty path element, trailing slash is added after the end
|
||||
r++
|
||||
|
||||
case p[r] == '.' && r+1 == n:
|
||||
trailing = true
|
||||
r++
|
||||
|
||||
case p[r] == '.' && p[r+1] == '/':
|
||||
// . element
|
||||
r += 2
|
||||
|
||||
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
|
||||
// .. element: remove to last /
|
||||
r += 3
|
||||
|
||||
if w > 1 {
|
||||
// can backtrack
|
||||
w--
|
||||
|
||||
if len(buf) == 0 {
|
||||
for w > 1 && p[w] != '/' {
|
||||
w--
|
||||
}
|
||||
} else {
|
||||
for w > 1 && buf[w] != '/' {
|
||||
w--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// Real path element.
|
||||
// Add slash if needed
|
||||
if w > 1 {
|
||||
bufApp(&buf, p, w, '/')
|
||||
w++
|
||||
}
|
||||
|
||||
// Copy element
|
||||
for r < n && p[r] != '/' {
|
||||
bufApp(&buf, p, w, p[r])
|
||||
w++
|
||||
r++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-append trailing slash
|
||||
if trailing && w > 1 {
|
||||
bufApp(&buf, p, w, '/')
|
||||
w++
|
||||
}
|
||||
|
||||
// If the original string was not modified (or only shortened at the end),
|
||||
// return the respective substring of the original string.
|
||||
// Otherwise return a new string from the buffer.
|
||||
if len(buf) == 0 {
|
||||
return p[:w]
|
||||
}
|
||||
return string(buf[:w])
|
||||
}
|
||||
|
||||
// Internal helper to lazily create a buffer if necessary.
|
||||
// Calls to this function get inlined.
|
||||
func bufApp(buf *[]byte, s string, w int, c byte) {
|
||||
b := *buf
|
||||
if len(b) == 0 {
|
||||
// No modification of the original string so far.
|
||||
// If the next character is the same as in the original string, we do
|
||||
// not yet have to allocate a buffer.
|
||||
if s[w] == c {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise use either the stack buffer, if it is large enough, or
|
||||
// allocate a new buffer on the heap, and copy all previous characters.
|
||||
if l := len(s); l > cap(b) {
|
||||
*buf = make([]byte, len(s))
|
||||
} else {
|
||||
*buf = (*buf)[:l]
|
||||
}
|
||||
b = *buf
|
||||
|
||||
copy(b, s[:w])
|
||||
}
|
||||
b[w] = c
|
||||
}
|
|
@ -0,0 +1,489 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Handle is a function that can be registered to a route to handle HTTP
|
||||
// requests. Like http.HandlerFunc, but has a third parameter for the values of
|
||||
// wildcards (path variables).
|
||||
type Handle func(http.ResponseWriter, *http.Request, Params)
|
||||
|
||||
// Param is a single URL parameter, consisting of a key and a value.
|
||||
type Param struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
// Params is a Param-slice, as returned by the router.
|
||||
// The slice is ordered, the first URL parameter is also the first slice value.
|
||||
// It is therefore safe to read values by the index.
|
||||
type Params []Param
|
||||
|
||||
// ByName returns the value of the first Param which key matches the given name.
|
||||
// If no matching Param is found, an empty string is returned.
|
||||
func (ps Params) ByName(name string) string {
|
||||
for _, p := range ps {
|
||||
if p.Key == name {
|
||||
return p.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type paramsKey struct{}
|
||||
|
||||
// ParamsKey is the request context key under which URL params are stored.
|
||||
var ParamsKey = paramsKey{}
|
||||
|
||||
// ParamsFromContext pulls the URL parameters from a request context,
|
||||
// or returns nil if none are present.
|
||||
func ParamsFromContext(ctx context.Context) Params {
|
||||
p, _ := ctx.Value(ParamsKey).(Params)
|
||||
return p
|
||||
}
|
||||
|
||||
// MatchedRoutePathParam is the Param name under which the path of the matched
|
||||
// route is stored, if Router.SaveMatchedRoutePath is set.
|
||||
var MatchedRoutePathParam = "$matchedRoutePath"
|
||||
|
||||
// MatchedRoutePath retrieves the path of the matched route.
|
||||
// Router.SaveMatchedRoutePath must have been enabled when the respective
|
||||
// handler was added, otherwise this function always returns an empty string.
|
||||
func (ps Params) MatchedRoutePath() string {
|
||||
return ps.ByName(MatchedRoutePathParam)
|
||||
}
|
||||
|
||||
// Router is a http.Handler which can be used to dispatch requests to different
|
||||
// handler functions via configurable routes
|
||||
type Router struct {
|
||||
trees map[string]*node
|
||||
|
||||
paramsPool sync.Pool
|
||||
maxParams uint16
|
||||
|
||||
// If enabled, adds the matched route path onto the http.Request context
|
||||
// before invoking the handler.
|
||||
// The matched route path is only added to handlers of routes that were
|
||||
// registered when this option was enabled.
|
||||
SaveMatchedRoutePath bool
|
||||
|
||||
// Enables automatic redirection if the current route can't be matched but a
|
||||
// handler for the path with (without) the trailing slash exists.
|
||||
// For example if /foo/ is requested but a route only exists for /foo, the
|
||||
// client is redirected to /foo with http status code 301 for GET requests
|
||||
// and 308 for all other request methods.
|
||||
RedirectTrailingSlash bool
|
||||
|
||||
// If enabled, the router tries to fix the current request path, if no
|
||||
// handle is registered for it.
|
||||
// First superfluous path elements like ../ or // are removed.
|
||||
// Afterwards the router does a case-insensitive lookup of the cleaned path.
|
||||
// If a handle can be found for this route, the router makes a redirection
|
||||
// to the corrected path with status code 301 for GET requests and 308 for
|
||||
// all other request methods.
|
||||
// For example /FOO and /..//Foo could be redirected to /foo.
|
||||
// RedirectTrailingSlash is independent of this option.
|
||||
RedirectFixedPath bool
|
||||
|
||||
// If enabled, the router checks if another method is allowed for the
|
||||
// current route, if the current request can not be routed.
|
||||
// If this is the case, the request is answered with 'Method Not Allowed'
|
||||
// and HTTP status code 405.
|
||||
// If no other Method is allowed, the request is delegated to the NotFound
|
||||
// handler.
|
||||
HandleMethodNotAllowed bool
|
||||
|
||||
// If enabled, the router automatically replies to OPTIONS requests.
|
||||
// Custom OPTIONS handlers take priority over automatic replies.
|
||||
HandleOPTIONS bool
|
||||
|
||||
// An optional http.Handler that is called on automatic OPTIONS requests.
|
||||
// The handler is only called if HandleOPTIONS is true and no OPTIONS
|
||||
// handler for the specific path was set.
|
||||
// The "Allowed" header is set before calling the handler.
|
||||
GlobalOPTIONS http.Handler
|
||||
|
||||
// Cached value of global (*) allowed methods
|
||||
globalAllowed string
|
||||
|
||||
// Configurable http.Handler which is called when no matching route is
|
||||
// found. If it is not set, http.NotFound is used.
|
||||
NotFound http.Handler
|
||||
|
||||
// Configurable http.Handler which is called when a request
|
||||
// cannot be routed and HandleMethodNotAllowed is true.
|
||||
// If it is not set, http.Error with http.StatusMethodNotAllowed is used.
|
||||
// The "Allow" header with allowed request methods is set before the handler
|
||||
// is called.
|
||||
MethodNotAllowed http.Handler
|
||||
|
||||
// Function to handle panics recovered from http handlers.
|
||||
// It should be used to generate a error page and return the http error code
|
||||
// 500 (Internal Server Error).
|
||||
// The handler can be used to keep your server from crashing because of
|
||||
// unrecovered panics.
|
||||
PanicHandler func(http.ResponseWriter, *http.Request, interface{})
|
||||
}
|
||||
|
||||
// Make sure the Router conforms with the http.Handler interface
|
||||
var _ http.Handler = New()
|
||||
|
||||
// New returns a new initialized Router.
|
||||
// Path auto-correction, including trailing slashes, is enabled by default.
|
||||
func New() *Router {
|
||||
return &Router{
|
||||
RedirectTrailingSlash: true,
|
||||
RedirectFixedPath: true,
|
||||
HandleMethodNotAllowed: true,
|
||||
HandleOPTIONS: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) getParams() *Params {
|
||||
ps, _ := r.paramsPool.Get().(*Params)
|
||||
*ps = (*ps)[0:0] // reset slice
|
||||
return ps
|
||||
}
|
||||
|
||||
func (r *Router) putParams(ps *Params) {
|
||||
if ps != nil {
|
||||
r.paramsPool.Put(ps)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) saveMatchedRoutePath(path string, handle Handle) Handle {
|
||||
return func(w http.ResponseWriter, req *http.Request, ps Params) {
|
||||
if ps == nil {
|
||||
psp := r.getParams()
|
||||
ps = (*psp)[0:1]
|
||||
ps[0] = Param{Key: MatchedRoutePathParam, Value: path}
|
||||
handle(w, req, ps)
|
||||
r.putParams(psp)
|
||||
} else {
|
||||
ps = append(ps, Param{Key: MatchedRoutePathParam, Value: path})
|
||||
handle(w, req, ps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GET is a shortcut for router.Handle(http.MethodGet, path, handle)
|
||||
func (r *Router) GET(path string, handle Handle) {
|
||||
r.Handle(http.MethodGet, path, handle)
|
||||
}
|
||||
|
||||
// HEAD is a shortcut for router.Handle(http.MethodHead, path, handle)
|
||||
func (r *Router) HEAD(path string, handle Handle) {
|
||||
r.Handle(http.MethodHead, path, handle)
|
||||
}
|
||||
|
||||
// OPTIONS is a shortcut for router.Handle(http.MethodOptions, path, handle)
|
||||
func (r *Router) OPTIONS(path string, handle Handle) {
|
||||
r.Handle(http.MethodOptions, path, handle)
|
||||
}
|
||||
|
||||
// POST is a shortcut for router.Handle(http.MethodPost, path, handle)
|
||||
func (r *Router) POST(path string, handle Handle) {
|
||||
r.Handle(http.MethodPost, path, handle)
|
||||
}
|
||||
|
||||
// PUT is a shortcut for router.Handle(http.MethodPut, path, handle)
|
||||
func (r *Router) PUT(path string, handle Handle) {
|
||||
r.Handle(http.MethodPut, path, handle)
|
||||
}
|
||||
|
||||
// PATCH is a shortcut for router.Handle(http.MethodPatch, path, handle)
|
||||
func (r *Router) PATCH(path string, handle Handle) {
|
||||
r.Handle(http.MethodPatch, path, handle)
|
||||
}
|
||||
|
||||
// DELETE is a shortcut for router.Handle(http.MethodDelete, path, handle)
|
||||
func (r *Router) DELETE(path string, handle Handle) {
|
||||
r.Handle(http.MethodDelete, path, handle)
|
||||
}
|
||||
|
||||
// Replace registers or replace request handle with the given path and method.
|
||||
//
|
||||
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
|
||||
// functions can be used.
|
||||
//
|
||||
// This function is intended for bulk loading and to allow the usage of less
|
||||
// frequently used, non-standardized or custom methods (e.g. for internal
|
||||
// communication with a proxy).
|
||||
func (r *Router) Replace(method, path string, handle Handle) {
|
||||
r.addRoute(method, path, true, handle)
|
||||
}
|
||||
|
||||
// Handle registers a new request handle with the given path and method.
|
||||
//
|
||||
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
|
||||
// functions can be used.
|
||||
//
|
||||
// This function is intended for bulk loading and to allow the usage of less
|
||||
// frequently used, non-standardized or custom methods (e.g. for internal
|
||||
// communication with a proxy).
|
||||
func (r *Router) Handle(method, path string, handle Handle) {
|
||||
r.addRoute(method, path, false, handle)
|
||||
}
|
||||
|
||||
// addRoute registers a new request handle with the given path and method.
|
||||
//
|
||||
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
|
||||
// functions can be used.
|
||||
//
|
||||
// This function is intended for bulk loading and to allow the usage of less
|
||||
// frequently used, non-standardized or custom methods (e.g. for internal
|
||||
// communication with a proxy).
|
||||
func (r *Router) addRoute(method, path string, replace bool, handle Handle) {
|
||||
varsCount := uint16(0)
|
||||
|
||||
if method == "" {
|
||||
panic("method must not be empty")
|
||||
}
|
||||
if len(path) < 1 || path[0] != '/' {
|
||||
panic("path must begin with '/' in path '" + path + "'")
|
||||
}
|
||||
if handle == nil {
|
||||
panic("handle must not be nil")
|
||||
}
|
||||
|
||||
if r.SaveMatchedRoutePath {
|
||||
varsCount++
|
||||
handle = r.saveMatchedRoutePath(path, handle)
|
||||
}
|
||||
|
||||
if r.trees == nil {
|
||||
r.trees = make(map[string]*node)
|
||||
}
|
||||
|
||||
root := r.trees[method]
|
||||
if root == nil {
|
||||
root = new(node)
|
||||
r.trees[method] = root
|
||||
|
||||
r.globalAllowed = r.allowed("*", "")
|
||||
}
|
||||
|
||||
root.addRoute(path, handle, replace)
|
||||
|
||||
// Update maxParams
|
||||
if paramsCount := countParams(path); paramsCount+varsCount > r.maxParams {
|
||||
r.maxParams = paramsCount + varsCount
|
||||
}
|
||||
|
||||
// Lazy-init paramsPool alloc func
|
||||
if r.paramsPool.New == nil && r.maxParams > 0 {
|
||||
r.paramsPool.New = func() interface{} {
|
||||
ps := make(Params, 0, r.maxParams)
|
||||
return &ps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handler is an adapter which allows the usage of an http.Handler as a
|
||||
// request handle.
|
||||
// The Params are available in the request context under ParamsKey.
|
||||
func (r *Router) Handler(method, path string, handler http.Handler) {
|
||||
r.Handle(method, path,
|
||||
func(w http.ResponseWriter, req *http.Request, p Params) {
|
||||
if len(p) > 0 {
|
||||
ctx := req.Context()
|
||||
ctx = context.WithValue(ctx, ParamsKey, p)
|
||||
req = req.WithContext(ctx)
|
||||
}
|
||||
handler.ServeHTTP(w, req)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a
|
||||
// request handle.
|
||||
func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) {
|
||||
r.Handler(method, path, handler)
|
||||
}
|
||||
|
||||
// ServeFiles serves files from the given file system root.
|
||||
// The path must end with "/*filepath", files are then served from the local
|
||||
// path /defined/root/dir/*filepath.
|
||||
// For example if root is "/etc" and *filepath is "passwd", the local file
|
||||
// "/etc/passwd" would be served.
|
||||
// Internally a http.FileServer is used, therefore http.NotFound is used instead
|
||||
// of the Router's NotFound handler.
|
||||
// To use the operating system's file system implementation,
|
||||
// use http.Dir:
|
||||
//
|
||||
// router.ServeFiles("/src/*filepath", http.Dir("/var/www"))
|
||||
func (r *Router) ServeFiles(path string, root http.FileSystem) {
|
||||
if len(path) < 10 || path[len(path)-10:] != "/*filepath" {
|
||||
panic("path must end with /*filepath in path '" + path + "'")
|
||||
}
|
||||
|
||||
fileServer := http.FileServer(root)
|
||||
|
||||
r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) {
|
||||
req.URL.Path = ps.ByName("filepath")
|
||||
fileServer.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Router) recv(w http.ResponseWriter, req *http.Request) {
|
||||
if rcv := recover(); rcv != nil {
|
||||
r.PanicHandler(w, req, rcv)
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup allows the manual lookup of a method + path combo.
|
||||
// This is e.g. useful to build a framework around this router.
|
||||
// If the path was found, it returns the handle function and the path parameter
|
||||
// values. Otherwise the third return value indicates whether a redirection to
|
||||
// the same path with an extra / without the trailing slash should be performed.
|
||||
func (r *Router) Lookup(method, path string) (Handle, Params, bool) {
|
||||
if root := r.trees[method]; root != nil {
|
||||
handle, ps, tsr := root.getValue(path, r.getParams)
|
||||
if handle == nil {
|
||||
r.putParams(ps)
|
||||
return nil, nil, tsr
|
||||
}
|
||||
if ps == nil {
|
||||
return handle, nil, tsr
|
||||
}
|
||||
return handle, *ps, tsr
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
func (r *Router) allowed(path, reqMethod string) (allow string) {
|
||||
allowed := make([]string, 0, 9)
|
||||
|
||||
if path == "*" { // server-wide
|
||||
// empty method is used for internal calls to refresh the cache
|
||||
if reqMethod == "" {
|
||||
for method := range r.trees {
|
||||
if method == http.MethodOptions {
|
||||
continue
|
||||
}
|
||||
// Add request method to list of allowed methods
|
||||
allowed = append(allowed, method)
|
||||
}
|
||||
} else {
|
||||
return r.globalAllowed
|
||||
}
|
||||
} else { // specific path
|
||||
for method := range r.trees {
|
||||
// Skip the requested method - we already tried this one
|
||||
if method == reqMethod || method == http.MethodOptions {
|
||||
continue
|
||||
}
|
||||
|
||||
handle, _, _ := r.trees[method].getValue(path, nil)
|
||||
if handle != nil {
|
||||
// Add request method to list of allowed methods
|
||||
allowed = append(allowed, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(allowed) > 0 {
|
||||
// Add request method to list of allowed methods
|
||||
allowed = append(allowed, http.MethodOptions)
|
||||
|
||||
// Sort allowed methods.
|
||||
// sort.Strings(allowed) unfortunately causes unnecessary allocations
|
||||
// due to allowed being moved to the heap and interface conversion
|
||||
for i, l := 1, len(allowed); i < l; i++ {
|
||||
for j := i; j > 0 && allowed[j] < allowed[j-1]; j-- {
|
||||
allowed[j], allowed[j-1] = allowed[j-1], allowed[j]
|
||||
}
|
||||
}
|
||||
|
||||
// return as comma separated list
|
||||
return strings.Join(allowed, ", ")
|
||||
}
|
||||
|
||||
return allow
|
||||
}
|
||||
|
||||
// ServeHTTP makes the router implement the http.Handler interface.
|
||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
if r.PanicHandler != nil {
|
||||
defer r.recv(w, req)
|
||||
}
|
||||
|
||||
path := req.URL.Path
|
||||
|
||||
if root := r.trees[req.Method]; root != nil {
|
||||
if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil {
|
||||
if ps != nil {
|
||||
handle(w, req, *ps)
|
||||
r.putParams(ps)
|
||||
} else {
|
||||
handle(w, req, nil)
|
||||
}
|
||||
return
|
||||
} else if req.Method != http.MethodConnect && path != "/" {
|
||||
// Moved Permanently, request with GET method
|
||||
code := http.StatusMovedPermanently
|
||||
if req.Method != http.MethodGet {
|
||||
// Permanent Redirect, request with same method
|
||||
code = http.StatusPermanentRedirect
|
||||
}
|
||||
|
||||
if tsr && r.RedirectTrailingSlash {
|
||||
if len(path) > 1 && path[len(path)-1] == '/' {
|
||||
req.URL.Path = path[:len(path)-1]
|
||||
} else {
|
||||
req.URL.Path = path + "/"
|
||||
}
|
||||
http.Redirect(w, req, req.URL.String(), code)
|
||||
return
|
||||
}
|
||||
|
||||
// Try to fix the request path
|
||||
if r.RedirectFixedPath {
|
||||
fixedPath, found := root.findCaseInsensitivePath(
|
||||
CleanPath(path),
|
||||
r.RedirectTrailingSlash,
|
||||
)
|
||||
if found {
|
||||
req.URL.Path = fixedPath
|
||||
http.Redirect(w, req, req.URL.String(), code)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Method == http.MethodOptions && r.HandleOPTIONS {
|
||||
// Handle OPTIONS requests
|
||||
if allow := r.allowed(path, http.MethodOptions); allow != "" {
|
||||
w.Header().Set("Allow", allow)
|
||||
if r.GlobalOPTIONS != nil {
|
||||
r.GlobalOPTIONS.ServeHTTP(w, req)
|
||||
}
|
||||
return
|
||||
}
|
||||
} else if r.HandleMethodNotAllowed { // Handle 405
|
||||
if allow := r.allowed(path, req.Method); allow != "" {
|
||||
w.Header().Set("Allow", allow)
|
||||
if r.MethodNotAllowed != nil {
|
||||
r.MethodNotAllowed.ServeHTTP(w, req)
|
||||
} else {
|
||||
http.Error(w,
|
||||
http.StatusText(http.StatusMethodNotAllowed),
|
||||
http.StatusMethodNotAllowed,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 404
|
||||
if r.NotFound != nil {
|
||||
r.NotFound.ServeHTTP(w, req)
|
||||
} else {
|
||||
http.NotFound(w, req)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,683 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func min(a, b int) int {
|
||||
if a <= b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func longestCommonPrefix(a, b string) int {
|
||||
i := 0
|
||||
max := min(len(a), len(b))
|
||||
for i < max && a[i] == b[i] {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// Search for a wildcard segment and check the name for invalid characters.
|
||||
// Returns -1 as index, if no wildcard was found.
|
||||
func findWildcard(path string) (wilcard string, i int, valid bool) {
|
||||
// Find start
|
||||
for start, c := range []byte(path) {
|
||||
// A wildcard starts with ':' (param) or '*' (catch-all)
|
||||
if c != ':' && c != '*' {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find end and check for invalid characters
|
||||
valid = true
|
||||
for end, c := range []byte(path[start+1:]) {
|
||||
switch c {
|
||||
case '/':
|
||||
return path[start : start+1+end], start, valid
|
||||
case ':', '*':
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
return path[start:], start, valid
|
||||
}
|
||||
return "", -1, false
|
||||
}
|
||||
|
||||
func countParams(path string) uint16 {
|
||||
var n uint
|
||||
for i := range []byte(path) {
|
||||
switch path[i] {
|
||||
case ':', '*':
|
||||
n++
|
||||
}
|
||||
}
|
||||
return uint16(n)
|
||||
}
|
||||
|
||||
type nodeType uint8
|
||||
|
||||
const (
|
||||
static nodeType = iota // default
|
||||
root
|
||||
param
|
||||
catchAll
|
||||
)
|
||||
|
||||
type node struct {
|
||||
path string
|
||||
indices string
|
||||
wildChild bool
|
||||
nType nodeType
|
||||
priority uint32
|
||||
children []*node
|
||||
handle Handle
|
||||
}
|
||||
|
||||
// Increments priority of the given child and reorders if necessary
|
||||
func (n *node) incrementChildPrio(pos int) int {
|
||||
cs := n.children
|
||||
cs[pos].priority++
|
||||
prio := cs[pos].priority
|
||||
|
||||
// Adjust position (move to front)
|
||||
newPos := pos
|
||||
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
|
||||
// Swap node positions
|
||||
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
|
||||
}
|
||||
|
||||
// Build new index char string
|
||||
if newPos != pos {
|
||||
n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
|
||||
n.indices[pos:pos+1] + // The index char we move
|
||||
n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
|
||||
}
|
||||
|
||||
return newPos
|
||||
}
|
||||
|
||||
// addRoute adds a node with the given handle to the path.
|
||||
// Not concurrency-safe!
|
||||
func (n *node) addRoute(path string, handle Handle, replace bool) {
|
||||
fullPath := path
|
||||
n.priority++
|
||||
|
||||
// Empty tree
|
||||
if n.path == "" && n.indices == "" {
|
||||
n.insertChild(path, fullPath, handle)
|
||||
n.nType = root
|
||||
return
|
||||
}
|
||||
|
||||
walk:
|
||||
for {
|
||||
// Find the longest common prefix.
|
||||
// This also implies that the common prefix contains no ':' or '*'
|
||||
// since the existing key can't contain those chars.
|
||||
i := longestCommonPrefix(path, n.path)
|
||||
|
||||
// Split edge
|
||||
if i < len(n.path) {
|
||||
child := node{
|
||||
path: n.path[i:],
|
||||
wildChild: n.wildChild,
|
||||
nType: static,
|
||||
indices: n.indices,
|
||||
children: n.children,
|
||||
handle: n.handle,
|
||||
priority: n.priority - 1,
|
||||
}
|
||||
|
||||
n.children = []*node{&child}
|
||||
// []byte for proper unicode char conversion, see #65
|
||||
n.indices = string([]byte{n.path[i]})
|
||||
n.path = path[:i]
|
||||
n.handle = nil
|
||||
n.wildChild = false
|
||||
}
|
||||
|
||||
// Make new node a child of this node
|
||||
if i < len(path) {
|
||||
path = path[i:]
|
||||
|
||||
if n.wildChild {
|
||||
n = n.children[0]
|
||||
n.priority++
|
||||
|
||||
// Check if the wildcard matches
|
||||
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
|
||||
// Adding a child to a catchAll is not possible
|
||||
n.nType != catchAll &&
|
||||
// Check for longer wildcard, e.g. :name and :names
|
||||
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
|
||||
continue walk
|
||||
} else {
|
||||
// Wildcard conflict
|
||||
pathSeg := path
|
||||
if n.nType != catchAll {
|
||||
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
|
||||
}
|
||||
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
|
||||
panic("'" + pathSeg +
|
||||
"' in new path '" + fullPath +
|
||||
"' conflicts with existing wildcard '" + n.path +
|
||||
"' in existing prefix '" + prefix +
|
||||
"'")
|
||||
}
|
||||
}
|
||||
|
||||
idxc := path[0]
|
||||
|
||||
// '/' after param
|
||||
if n.nType == param && idxc == '/' && len(n.children) == 1 {
|
||||
n = n.children[0]
|
||||
n.priority++
|
||||
continue walk
|
||||
}
|
||||
|
||||
// Check if a child with the next path byte exists
|
||||
for i, c := range []byte(n.indices) {
|
||||
if c == idxc {
|
||||
i = n.incrementChildPrio(i)
|
||||
n = n.children[i]
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise insert it
|
||||
if idxc != ':' && idxc != '*' {
|
||||
// []byte for proper unicode char conversion, see #65
|
||||
n.indices += string([]byte{idxc})
|
||||
child := &node{}
|
||||
n.children = append(n.children, child)
|
||||
n.incrementChildPrio(len(n.indices) - 1)
|
||||
n = child
|
||||
}
|
||||
n.insertChild(path, fullPath, handle)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise add handle to current node
|
||||
if !replace {
|
||||
if n.handle != nil {
|
||||
panic("a handle is already registered for path '" + fullPath + "'")
|
||||
}
|
||||
}
|
||||
n.handle = handle
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (n *node) insertChild(path, fullPath string, handle Handle) {
|
||||
for {
|
||||
// Find prefix until first wildcard
|
||||
wildcard, i, valid := findWildcard(path)
|
||||
if i < 0 { // No wilcard found
|
||||
break
|
||||
}
|
||||
|
||||
// The wildcard name must not contain ':' and '*'
|
||||
if !valid {
|
||||
panic("only one wildcard per path segment is allowed, has: '" +
|
||||
wildcard + "' in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
// Check if the wildcard has a name
|
||||
if len(wildcard) < 2 {
|
||||
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
// Check if this node has existing children which would be
|
||||
// unreachable if we insert the wildcard here
|
||||
if len(n.children) > 0 {
|
||||
panic("wildcard segment '" + wildcard +
|
||||
"' conflicts with existing children in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
// param
|
||||
if wildcard[0] == ':' {
|
||||
if i > 0 {
|
||||
// Insert prefix before the current wildcard
|
||||
n.path = path[:i]
|
||||
path = path[i:]
|
||||
}
|
||||
|
||||
n.wildChild = true
|
||||
child := &node{
|
||||
nType: param,
|
||||
path: wildcard,
|
||||
}
|
||||
n.children = []*node{child}
|
||||
n = child
|
||||
n.priority++
|
||||
|
||||
// If the path doesn't end with the wildcard, then there
|
||||
// will be another non-wildcard subpath starting with '/'
|
||||
if len(wildcard) < len(path) {
|
||||
path = path[len(wildcard):]
|
||||
child := &node{
|
||||
priority: 1,
|
||||
}
|
||||
n.children = []*node{child}
|
||||
n = child
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise we're done. Insert the handle in the new leaf
|
||||
n.handle = handle
|
||||
return
|
||||
}
|
||||
|
||||
// catchAll
|
||||
if i+len(wildcard) != len(path) {
|
||||
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
||||
panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
// Currently fixed width 1 for '/'
|
||||
i--
|
||||
if path[i] != '/' {
|
||||
panic("no / before catch-all in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
n.path = path[:i]
|
||||
|
||||
// First node: catchAll node with empty path
|
||||
child := &node{
|
||||
wildChild: true,
|
||||
nType: catchAll,
|
||||
}
|
||||
n.children = []*node{child}
|
||||
n.indices = string('/')
|
||||
n = child
|
||||
n.priority++
|
||||
|
||||
// Second node: node holding the variable
|
||||
child = &node{
|
||||
path: path[i:],
|
||||
nType: catchAll,
|
||||
handle: handle,
|
||||
priority: 1,
|
||||
}
|
||||
n.children = []*node{child}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// If no wildcard was found, simply insert the path and handle
|
||||
n.path = path
|
||||
n.handle = handle
|
||||
}
|
||||
|
||||
// Returns the handle registered with the given path (key). The values of
|
||||
// wildcards are saved to a map.
|
||||
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
|
||||
// made if a handle exists with an extra (without the) trailing slash for the
|
||||
// given path.
|
||||
func (n *node) getValue(path string, params func() *Params) (handle Handle, ps *Params, tsr bool) {
|
||||
walk: // Outer loop for walking the tree
|
||||
for {
|
||||
prefix := n.path
|
||||
if len(path) > len(prefix) {
|
||||
if path[:len(prefix)] == prefix {
|
||||
path = path[len(prefix):]
|
||||
|
||||
// If this node does not have a wildcard (param or catchAll)
|
||||
// child, we can just look up the next child node and continue
|
||||
// to walk down the tree
|
||||
if !n.wildChild {
|
||||
idxc := path[0]
|
||||
for i, c := range []byte(n.indices) {
|
||||
if c == idxc {
|
||||
n = n.children[i]
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found.
|
||||
// We can recommend to redirect to the same URL without a
|
||||
// trailing slash if a leaf exists for that path.
|
||||
tsr = (path == "/" && n.handle != nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle wildcard child
|
||||
n = n.children[0]
|
||||
switch n.nType {
|
||||
case param:
|
||||
// Find param end (either '/' or path end)
|
||||
end := 0
|
||||
for end < len(path) && path[end] != '/' {
|
||||
end++
|
||||
}
|
||||
|
||||
// Save param value
|
||||
if params != nil {
|
||||
if ps == nil {
|
||||
ps = params()
|
||||
}
|
||||
// Expand slice within preallocated capacity
|
||||
i := len(*ps)
|
||||
*ps = (*ps)[:i+1]
|
||||
(*ps)[i] = Param{
|
||||
Key: n.path[1:],
|
||||
Value: path[:end],
|
||||
}
|
||||
}
|
||||
|
||||
// We need to go deeper!
|
||||
if end < len(path) {
|
||||
if len(n.children) > 0 {
|
||||
path = path[end:]
|
||||
n = n.children[0]
|
||||
continue walk
|
||||
}
|
||||
|
||||
// ... but we can't
|
||||
tsr = (len(path) == end+1)
|
||||
return
|
||||
}
|
||||
|
||||
if handle = n.handle; handle != nil {
|
||||
return
|
||||
} else if len(n.children) == 1 {
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists for TSR recommendation
|
||||
n = n.children[0]
|
||||
tsr = (n.path == "/" && n.handle != nil) || (n.path == "" && n.indices == "/")
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
case catchAll:
|
||||
// Save param value
|
||||
if params != nil {
|
||||
if ps == nil {
|
||||
ps = params()
|
||||
}
|
||||
// Expand slice within preallocated capacity
|
||||
i := len(*ps)
|
||||
*ps = (*ps)[:i+1]
|
||||
(*ps)[i] = Param{
|
||||
Key: n.path[2:],
|
||||
Value: path,
|
||||
}
|
||||
}
|
||||
|
||||
handle = n.handle
|
||||
return
|
||||
|
||||
default:
|
||||
panic("invalid node type")
|
||||
}
|
||||
}
|
||||
} else if path == prefix {
|
||||
// We should have reached the node containing the handle.
|
||||
// Check if this node has a handle registered.
|
||||
if handle = n.handle; handle != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If there is no handle for this route, but this route has a
|
||||
// wildcard child, there must be a handle for this path with an
|
||||
// additional trailing slash
|
||||
if path == "/" && n.wildChild && n.nType != root {
|
||||
tsr = true
|
||||
return
|
||||
}
|
||||
|
||||
if path == "/" && n.nType == static {
|
||||
tsr = true
|
||||
return
|
||||
}
|
||||
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists for trailing slash recommendation
|
||||
for i, c := range []byte(n.indices) {
|
||||
if c == '/' {
|
||||
n = n.children[i]
|
||||
tsr = (len(n.path) == 1 && n.handle != nil) ||
|
||||
(n.nType == catchAll && n.children[0].handle != nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Nothing found. We can recommend to redirect to the same URL with an
|
||||
// extra trailing slash if a leaf exists for that path
|
||||
tsr = (path == "/") ||
|
||||
(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
|
||||
path == prefix[:len(prefix)-1] && n.handle != nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Makes a case-insensitive lookup of the given path and tries to find a handler.
|
||||
// It can optionally also fix trailing slashes.
|
||||
// It returns the case-corrected path and a bool indicating whether the lookup
|
||||
// was successful.
|
||||
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (fixedPath string, found bool) {
|
||||
const stackBufSize = 128
|
||||
|
||||
// Use a static sized buffer on the stack in the common case.
|
||||
// If the path is too long, allocate a buffer on the heap instead.
|
||||
buf := make([]byte, 0, stackBufSize)
|
||||
if l := len(path) + 1; l > stackBufSize {
|
||||
buf = make([]byte, 0, l)
|
||||
}
|
||||
|
||||
ciPath := n.findCaseInsensitivePathRec(
|
||||
path,
|
||||
buf, // Preallocate enough memory for new path
|
||||
[4]byte{}, // Empty rune buffer
|
||||
fixTrailingSlash,
|
||||
)
|
||||
|
||||
return string(ciPath), ciPath != nil
|
||||
}
|
||||
|
||||
// Shift bytes in array by n bytes left
|
||||
func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
|
||||
switch n {
|
||||
case 0:
|
||||
return rb
|
||||
case 1:
|
||||
return [4]byte{rb[1], rb[2], rb[3], 0}
|
||||
case 2:
|
||||
return [4]byte{rb[2], rb[3]}
|
||||
case 3:
|
||||
return [4]byte{rb[3]}
|
||||
default:
|
||||
return [4]byte{}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursive case-insensitive lookup function used by n.findCaseInsensitivePath
|
||||
func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte {
|
||||
npLen := len(n.path)
|
||||
|
||||
walk: // Outer loop for walking the tree
|
||||
for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) {
|
||||
// Add common prefix to result
|
||||
oldPath := path
|
||||
path = path[npLen:]
|
||||
ciPath = append(ciPath, n.path...)
|
||||
|
||||
if len(path) > 0 {
|
||||
// If this node does not have a wildcard (param or catchAll) child,
|
||||
// we can just look up the next child node and continue to walk down
|
||||
// the tree
|
||||
if !n.wildChild {
|
||||
// Skip rune bytes already processed
|
||||
rb = shiftNRuneBytes(rb, npLen)
|
||||
|
||||
if rb[0] != 0 {
|
||||
// Old rune not finished
|
||||
idxc := rb[0]
|
||||
for i, c := range []byte(n.indices) {
|
||||
if c == idxc {
|
||||
// continue with child node
|
||||
n = n.children[i]
|
||||
npLen = len(n.path)
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Process a new rune
|
||||
var rv rune
|
||||
|
||||
// Find rune start.
|
||||
// Runes are up to 4 byte long,
|
||||
// -4 would definitely be another rune.
|
||||
var off int
|
||||
for max := min(npLen, 3); off < max; off++ {
|
||||
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
|
||||
// read rune from cached path
|
||||
rv, _ = utf8.DecodeRuneInString(oldPath[i:])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate lowercase bytes of current rune
|
||||
lo := unicode.ToLower(rv)
|
||||
utf8.EncodeRune(rb[:], lo)
|
||||
|
||||
// Skip already processed bytes
|
||||
rb = shiftNRuneBytes(rb, off)
|
||||
|
||||
idxc := rb[0]
|
||||
for i, c := range []byte(n.indices) {
|
||||
// Lowercase matches
|
||||
if c == idxc {
|
||||
// must use a recursive approach since both the
|
||||
// uppercase byte and the lowercase byte might exist
|
||||
// as an index
|
||||
if out := n.children[i].findCaseInsensitivePathRec(
|
||||
path, ciPath, rb, fixTrailingSlash,
|
||||
); out != nil {
|
||||
return out
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we found no match, the same for the uppercase rune,
|
||||
// if it differs
|
||||
if up := unicode.ToUpper(rv); up != lo {
|
||||
utf8.EncodeRune(rb[:], up)
|
||||
rb = shiftNRuneBytes(rb, off)
|
||||
|
||||
idxc := rb[0]
|
||||
for i, c := range []byte(n.indices) {
|
||||
// Uppercase matches
|
||||
if c == idxc {
|
||||
// Continue with child node
|
||||
n = n.children[i]
|
||||
npLen = len(n.path)
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found. We can recommend to redirect to the same URL
|
||||
// without a trailing slash if a leaf exists for that path
|
||||
if fixTrailingSlash && path == "/" && n.handle != nil {
|
||||
return ciPath
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
n = n.children[0]
|
||||
switch n.nType {
|
||||
case param:
|
||||
// Find param end (either '/' or path end)
|
||||
end := 0
|
||||
for end < len(path) && path[end] != '/' {
|
||||
end++
|
||||
}
|
||||
|
||||
// Add param value to case insensitive path
|
||||
ciPath = append(ciPath, path[:end]...)
|
||||
|
||||
// We need to go deeper!
|
||||
if end < len(path) {
|
||||
if len(n.children) > 0 {
|
||||
// Continue with child node
|
||||
n = n.children[0]
|
||||
npLen = len(n.path)
|
||||
path = path[end:]
|
||||
continue
|
||||
}
|
||||
|
||||
// ... but we can't
|
||||
if fixTrailingSlash && len(path) == end+1 {
|
||||
return ciPath
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if n.handle != nil {
|
||||
return ciPath
|
||||
} else if fixTrailingSlash && len(n.children) == 1 {
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists
|
||||
n = n.children[0]
|
||||
if n.path == "/" && n.handle != nil {
|
||||
return append(ciPath, '/')
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
case catchAll:
|
||||
return append(ciPath, path...)
|
||||
|
||||
default:
|
||||
panic("invalid node type")
|
||||
}
|
||||
} else {
|
||||
// We should have reached the node containing the handle.
|
||||
// Check if this node has a handle registered.
|
||||
if n.handle != nil {
|
||||
return ciPath
|
||||
}
|
||||
|
||||
// No handle found.
|
||||
// Try to fix the path by adding a trailing slash
|
||||
if fixTrailingSlash {
|
||||
for i, c := range []byte(n.indices) {
|
||||
if c == '/' {
|
||||
n = n.children[i]
|
||||
if (len(n.path) == 1 && n.handle != nil) ||
|
||||
(n.nType == catchAll && n.children[0].handle != nil) {
|
||||
return append(ciPath, '/')
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found.
|
||||
// Try to fix the path by adding / removing a trailing slash
|
||||
if fixTrailingSlash {
|
||||
if path == "/" {
|
||||
return ciPath
|
||||
}
|
||||
if len(path)+1 == npLen && n.path[len(path)] == '/' &&
|
||||
strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handle != nil {
|
||||
return append(ciPath, n.path...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"git.nspix.com/golang/kos/entry/http/router"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
ctxPool sync.Pool
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
ctx context.Context
|
||||
serve *http.Server
|
||||
router *router.Router
|
||||
middleware []Middleware
|
||||
}
|
||||
|
||||
func (svr *Server) applyContext() *Context {
|
||||
if v := ctxPool.Get(); v != nil {
|
||||
if ctx, ok := v.(*Context); ok {
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
return &Context{}
|
||||
}
|
||||
|
||||
func (svr *Server) releaseContext(ctx *Context) {
|
||||
ctxPool.Put(ctx)
|
||||
}
|
||||
|
||||
func (svr *Server) wrapHandle(cb HandleFunc, middleware ...Middleware) router.Handle {
|
||||
return func(writer http.ResponseWriter, request *http.Request, params router.Params) {
|
||||
ctx := svr.applyContext()
|
||||
defer func() {
|
||||
svr.releaseContext(ctx)
|
||||
}()
|
||||
ps := make(map[string]string)
|
||||
for _, v := range params {
|
||||
ps[v.Key] = v.Value
|
||||
}
|
||||
ctx.reset(request, writer, ps)
|
||||
for i := len(svr.middleware) - 1; i >= 0; i-- {
|
||||
cb = svr.middleware[i](cb)
|
||||
}
|
||||
for i := len(middleware) - 1; i >= 0; i-- {
|
||||
cb = middleware[i](cb)
|
||||
}
|
||||
if err := cb(ctx); err != nil {
|
||||
ctx.Status(http.StatusServiceUnavailable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (svr *Server) Use(middleware ...Middleware) {
|
||||
svr.middleware = append(svr.middleware, middleware...)
|
||||
}
|
||||
|
||||
func (svr *Server) Handle(method string, path string, cb HandleFunc, middleware ...Middleware) {
|
||||
if method == "" {
|
||||
method = http.MethodPost
|
||||
}
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
svr.router.Replace(method, path, svr.wrapHandle(cb, middleware...))
|
||||
}
|
||||
|
||||
func (svr *Server) Group(prefix string, routes []Route, middleware ...Middleware) {
|
||||
for _, route := range routes {
|
||||
svr.Handle(route.Method, path.Join(prefix, route.Path), route.Handle, middleware...)
|
||||
}
|
||||
}
|
||||
|
||||
func (svr *Server) Embed(prefix string, root string, embedFs embed.FS) {
|
||||
routePath := prefix
|
||||
if !strings.HasSuffix(routePath, "/*filepath") {
|
||||
if strings.HasSuffix(routePath, "/") {
|
||||
routePath += "/"
|
||||
} else {
|
||||
routePath += "/*filepath"
|
||||
}
|
||||
}
|
||||
httpFs := http.FS(embedFs)
|
||||
svr.Handle(MethodGet, routePath, func(ctx *Context) (err error) {
|
||||
filename := strings.TrimPrefix(ctx.Request().URL.Path, prefix)
|
||||
if filename == "" || filename == "/" {
|
||||
filename = root + "/"
|
||||
if !strings.HasSuffix(filename, "/") {
|
||||
filename = filename + "/"
|
||||
}
|
||||
} else {
|
||||
if !strings.HasPrefix(filename, root) {
|
||||
filename = path.Clean(path.Join(root, filename))
|
||||
}
|
||||
}
|
||||
if !strings.HasPrefix(filename, "/") {
|
||||
filename = "/" + filename
|
||||
}
|
||||
ctx.Request().URL.Path = filename
|
||||
http.FileServer(httpFs).ServeHTTP(ctx.Response(), ctx.Request())
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func (svr *Server) Static(path string, root http.FileSystem) {
|
||||
if !strings.HasSuffix(path, "/*filepath") {
|
||||
if strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
} else {
|
||||
path += "/*filepath"
|
||||
}
|
||||
}
|
||||
svr.router.ServeFiles(path, root)
|
||||
}
|
||||
|
||||
func (svr *Server) handleOption(res http.ResponseWriter, req *http.Request) {
|
||||
res.Header().Add("Vary", "Origin")
|
||||
res.Header().Add("Vary", "Access-Control-Request-Method")
|
||||
res.Header().Add("Vary", "Access-Control-Request-Headers")
|
||||
res.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
res.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
res.Header().Set("Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE")
|
||||
h := req.Header.Get("Access-Control-Request-Headers")
|
||||
if h != "" {
|
||||
res.Header().Set("Access-Control-Allow-Headers", h)
|
||||
}
|
||||
res.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (svr *Server) handleRequest(res http.ResponseWriter, req *http.Request) {
|
||||
res.Header().Add("Vary", "Origin")
|
||||
res.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
res.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
h := req.Header.Get("Access-Control-Request-Headers")
|
||||
if h != "" {
|
||||
res.Header().Set("Access-Control-Allow-Headers", h)
|
||||
}
|
||||
svr.router.ServeHTTP(res, req)
|
||||
}
|
||||
|
||||
func (svr *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
switch request.Method {
|
||||
case http.MethodOptions:
|
||||
svr.handleOption(writer, request)
|
||||
default:
|
||||
svr.handleRequest(writer, request)
|
||||
}
|
||||
}
|
||||
|
||||
func (svr *Server) Serve(l net.Listener) (err error) {
|
||||
svr.serve = &http.Server{
|
||||
Handler: svr,
|
||||
}
|
||||
svr.router.NotFound = NotFound{}
|
||||
svr.router.MethodNotAllowed = NotAllowed{}
|
||||
return svr.serve.Serve(l)
|
||||
}
|
||||
|
||||
func (svr *Server) Shutdown() (err error) {
|
||||
if svr.serve != nil {
|
||||
err = svr.serve.Shutdown(svr.ctx)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func New(ctx context.Context) *Server {
|
||||
svr := &Server{
|
||||
ctx: ctx,
|
||||
router: router.New(),
|
||||
middleware: make([]Middleware, 0, 10),
|
||||
}
|
||||
return svr
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
type responsePayload struct {
|
||||
Code int `json:"code"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type HandleFunc func(ctx *Context) (err error)
|
||||
|
||||
type Middleware func(next HandleFunc) HandleFunc
|
||||
|
||||
type Route struct {
|
||||
Method string
|
||||
Path string
|
||||
Handle HandleFunc
|
||||
}
|
||||
|
||||
func Wrap(f http.HandlerFunc) HandleFunc {
|
||||
return func(ctx *Context) (err error) {
|
||||
f(ctx.Response(), ctx.Request())
|
||||
return
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package entry
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Listener struct {
|
||||
addr net.Addr
|
||||
ch chan net.Conn
|
||||
exitFlag int32
|
||||
exitChan chan struct{}
|
||||
}
|
||||
|
||||
func (l *Listener) Receive(conn net.Conn) {
|
||||
select {
|
||||
case l.ch <- conn:
|
||||
case <-l.exitChan:
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) Accept() (net.Conn, error) {
|
||||
select {
|
||||
case conn, ok := <-l.ch:
|
||||
if ok {
|
||||
return conn, nil
|
||||
} else {
|
||||
return nil, io.ErrClosedPipe
|
||||
}
|
||||
case <-l.exitChan:
|
||||
return nil, io.ErrClosedPipe
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) Close() error {
|
||||
if atomic.CompareAndSwapInt32(&l.exitFlag, 0, 1) {
|
||||
close(l.exitChan)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) Addr() net.Addr {
|
||||
return l.addr
|
||||
}
|
||||
|
||||
func newListener(addr net.Addr) *Listener {
|
||||
return &Listener{
|
||||
addr: addr,
|
||||
ch: make(chan net.Conn, 10),
|
||||
exitChan: make(chan struct{}),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package entry
|
||||
|
||||
type State struct {
|
||||
Accepting int32 `json:"accepting"` //是否正在接收连接
|
||||
Processing int32 `json:"processing"` //是否正在处理连接
|
||||
Concurrency int32 `json:"concurrency"`
|
||||
Request struct {
|
||||
Total int64 `json:"total"` //总处理请求
|
||||
Processed int64 `json:"processed"` //处理完成的请求
|
||||
Discarded int64 `json:"discarded"` //丢弃的请求
|
||||
} `json:"request"`
|
||||
Traffic struct {
|
||||
In int64 `json:"in"` //入网流量
|
||||
Out int64 `json:"out"` //出网流量
|
||||
} `json:"traffic"`
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
module git.nspix.com/golang/kos
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/mattn/go-runewidth v0.0.3
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/peterh/liner v1.2.2
|
||||
github.com/sourcegraph/conc v0.3.0
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect
|
|
@ -0,0 +1,14 @@
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
|
||||
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI=
|
||||
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
@ -0,0 +1,43 @@
|
|||
package kos
|
||||
|
||||
import (
|
||||
"git.nspix.com/golang/kos/entry/cli"
|
||||
"git.nspix.com/golang/kos/entry/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
std *application
|
||||
)
|
||||
|
||||
func initApplication(cbs ...Option) {
|
||||
once.Do(func() {
|
||||
std = New(cbs...)
|
||||
})
|
||||
}
|
||||
|
||||
func Init(cbs ...Option) *application {
|
||||
initApplication(cbs...)
|
||||
return std
|
||||
}
|
||||
|
||||
func Node() *Info {
|
||||
initApplication()
|
||||
return std.Info()
|
||||
}
|
||||
|
||||
func Http() *http.Server {
|
||||
initApplication()
|
||||
return std.Http()
|
||||
}
|
||||
|
||||
func Command() *cli.Server {
|
||||
initApplication()
|
||||
return std.Command()
|
||||
}
|
||||
|
||||
func Handle(method string, cb HandleFunc) {
|
||||
initApplication()
|
||||
std.Handle(method, cb)
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package kos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.nspix.com/golang/kos/util/env"
|
||||
"git.nspix.com/golang/kos/util/ip"
|
||||
"os"
|
||||
"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
|
||||
}
|
||||
|
||||
Option func(o *Options)
|
||||
)
|
||||
|
||||
func (o *Options) ShortName() string {
|
||||
if o.shortName != "" {
|
||||
return o.shortName
|
||||
}
|
||||
if pos := strings.LastIndex(o.Name, "/"); pos != -1 {
|
||||
o.shortName = o.Name[pos+1:]
|
||||
} else {
|
||||
o.shortName = o.Name
|
||||
}
|
||||
return o.shortName
|
||||
}
|
||||
|
||||
func WithName(name string, version string) Option {
|
||||
return func(o *Options) {
|
||||
o.Name = name
|
||||
o.Version = version
|
||||
}
|
||||
}
|
||||
|
||||
func WithPort(port int) Option {
|
||||
return func(o *Options) {
|
||||
o.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
func WithServer(s Server) Option {
|
||||
return func(o *Options) {
|
||||
o.server = s
|
||||
}
|
||||
}
|
||||
|
||||
func WithDebug() Option {
|
||||
return func(o *Options) {
|
||||
o.EnableDebug = true
|
||||
}
|
||||
}
|
||||
|
||||
func NewOptions() *Options {
|
||||
opts := &Options{
|
||||
Name: env.Get(EnvAppName, ""),
|
||||
Version: env.Get(EnvAppVersion, "0.0.1"),
|
||||
Context: context.Background(),
|
||||
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())
|
||||
return opts
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
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)
|
||||
Del(ctx context.Context, key string)
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
std Cache
|
||||
)
|
||||
|
||||
func init() {
|
||||
std = NewMemCache()
|
||||
}
|
||||
|
||||
func SetCache(l Cache) {
|
||||
std = l
|
||||
}
|
||||
|
||||
func GetCache() Cache {
|
||||
return std
|
||||
}
|
||||
|
||||
func Set(ctx context.Context, key string, value any) {
|
||||
std.Set(ctx, key, value)
|
||||
}
|
||||
|
||||
func SetEx(ctx context.Context, key string, value any, expire time.Duration) {
|
||||
std.SetEx(ctx, key, value, expire)
|
||||
}
|
||||
|
||||
func Get(ctx context.Context, key string) (value any, ok bool) {
|
||||
return std.Get(ctx, key)
|
||||
}
|
||||
|
||||
func Del(ctx context.Context, key string) {
|
||||
std.Del(ctx, key)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"time"
|
||||
)
|
||||
|
||||
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) SetEx(ctx context.Context, key string, value any, expire time.Duration) {
|
||||
cache.engine.Set(key, value, expire)
|
||||
}
|
||||
|
||||
func (cache *MemCache) Get(ctx context.Context, key string) (value any, ok bool) {
|
||||
return cache.engine.Get(key)
|
||||
}
|
||||
|
||||
func (cache *MemCache) Del(ctx context.Context, key string) {
|
||||
cache.engine.Delete(key)
|
||||
}
|
||||
|
||||
func NewMemCache() *MemCache {
|
||||
return &MemCache{
|
||||
engine: cache.New(time.Hour, time.Minute*90),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
FG_BLACK int = 30
|
||||
FG_RED = 31
|
||||
FG_GREEN = 32
|
||||
FG_YELLOW = 33
|
||||
FG_BLUE = 34
|
||||
FG_PURPLE = 35
|
||||
FG_CYAN = 36
|
||||
FG_GREY = 37
|
||||
)
|
||||
|
||||
type Console struct {
|
||||
Level int
|
||||
EnableColor int
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (log *Console) SetLevel(lv int) {
|
||||
log.Level = lv
|
||||
}
|
||||
|
||||
func (log *Console) Prefix(s string) {
|
||||
log.prefix = s
|
||||
}
|
||||
|
||||
func (log *Console) Print(i ...interface{}) {
|
||||
log.write(TraceLevel, fmt.Sprint(i...))
|
||||
}
|
||||
|
||||
func (log *Console) Printf(format string, args ...interface{}) {
|
||||
log.write(TraceLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (log *Console) Debug(i ...interface{}) {
|
||||
log.write(DebugLevel, fmt.Sprint(i...))
|
||||
}
|
||||
|
||||
func (log *Console) Debugf(format string, args ...interface{}) {
|
||||
log.write(DebugLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (log *Console) Info(i ...interface{}) {
|
||||
log.write(InfoLevel, fmt.Sprint(i...))
|
||||
}
|
||||
|
||||
func (log *Console) Infof(format string, args ...interface{}) {
|
||||
log.write(InfoLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (log *Console) Warn(i ...interface{}) {
|
||||
log.write(WarnLevel, fmt.Sprint(i...))
|
||||
}
|
||||
|
||||
func (log *Console) Warnf(format string, args ...interface{}) {
|
||||
log.write(WarnLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (log *Console) Error(i ...interface{}) {
|
||||
log.write(ErrorLevel, fmt.Sprint(i...))
|
||||
}
|
||||
|
||||
func (log *Console) Errorf(format string, args ...interface{}) {
|
||||
log.write(ErrorLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (log *Console) Fatal(i ...interface{}) {
|
||||
log.write(FatalLevel, fmt.Sprint(i...))
|
||||
}
|
||||
|
||||
func (log *Console) Fatalf(format string, args ...interface{}) {
|
||||
log.write(FatalLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (log *Console) Panic(i ...interface{}) {
|
||||
log.write(PanicLevel, fmt.Sprint(i...))
|
||||
}
|
||||
|
||||
func (log *Console) Panicf(format string, args ...interface{}) {
|
||||
log.write(PanicLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (log *Console) write(level int, s 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 {
|
||||
ls = getLevelText(level)
|
||||
}
|
||||
if log.prefix != "" {
|
||||
ls += " [" + log.prefix + "]"
|
||||
}
|
||||
fmt.Println(time.Now().Format("2006-01-02 15:04:05") + " " + ls + " " + s)
|
||||
}
|
||||
|
||||
func NewConsoleLogger() *Console {
|
||||
return &Console{
|
||||
EnableColor: 1,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,251 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
Filename string `json:"filename"`
|
||||
MaxSize int64 `json:"max_size"`
|
||||
MaxLogFiles int `json:"max_log_files"`
|
||||
Format string `json:"format"`
|
||||
Level int `json:"level"`
|
||||
mutex sync.RWMutex
|
||||
buf []byte
|
||||
fp *os.File
|
||||
prefix string
|
||||
size int64
|
||||
}
|
||||
|
||||
func itoa(buf *[]byte, i int, wid int) {
|
||||
// Assemble decimal in reverse order.
|
||||
var b [20]byte
|
||||
bp := len(b) - 1
|
||||
for i >= 10 || wid > 1 {
|
||||
wid--
|
||||
q := i / 10
|
||||
b[bp] = byte('0' + i - q*10)
|
||||
bp--
|
||||
i = q
|
||||
}
|
||||
// i < 10
|
||||
b[bp] = byte('0' + i)
|
||||
*buf = append(*buf, b[bp:]...)
|
||||
}
|
||||
|
||||
func (lg *File) SetLevel(lv int) {
|
||||
lg.Level = lv
|
||||
}
|
||||
|
||||
func (lg *File) Prefix(s string) {
|
||||
lg.prefix = s
|
||||
}
|
||||
|
||||
func (lg *File) Print(i ...interface{}) {
|
||||
lg.write(TraceLevel, fmt.Sprint(i...))
|
||||
}
|
||||
|
||||
func (lg *File) Printf(format string, args ...interface{}) {
|
||||
lg.write(TraceLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (lg *File) Debug(i ...interface{}) {
|
||||
lg.write(DebugLevel, fmt.Sprint(i...))
|
||||
}
|
||||
|
||||
func (lg *File) Debugf(format string, args ...interface{}) {
|
||||
lg.write(DebugLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (lg *File) Info(i ...interface{}) {
|
||||
lg.write(InfoLevel, fmt.Sprint(i...))
|
||||
}
|
||||
|
||||
func (lg *File) Infof(format string, args ...interface{}) {
|
||||
lg.write(InfoLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (lg *File) Warn(i ...interface{}) {
|
||||
lg.write(WarnLevel, fmt.Sprint(i...))
|
||||
}
|
||||
|
||||
func (lg *File) Warnf(format string, args ...interface{}) {
|
||||
lg.write(WarnLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (lg *File) Error(i ...interface{}) {
|
||||
lg.write(ErrorLevel, fmt.Sprint(i...))
|
||||
}
|
||||
|
||||
func (lg *File) Errorf(format string, args ...interface{}) {
|
||||
lg.write(ErrorLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (lg *File) Fatal(i ...interface{}) {
|
||||
lg.write(FatalLevel, fmt.Sprint(i...))
|
||||
}
|
||||
|
||||
func (lg *File) Fatalf(format string, args ...interface{}) {
|
||||
lg.write(FatalLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (lg *File) Panic(i ...interface{}) {
|
||||
lg.write(PanicLevel, fmt.Sprint(i...))
|
||||
}
|
||||
|
||||
func (lg *File) Panicf(format string, args ...interface{}) {
|
||||
lg.write(PanicLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (lg *File) format(buf *[]byte, level int, s string) (err error) {
|
||||
t := time.Now()
|
||||
year, month, day := t.Date()
|
||||
itoa(buf, year, 4)
|
||||
*buf = append(*buf, '-')
|
||||
itoa(buf, int(month), 2)
|
||||
*buf = append(*buf, '-')
|
||||
itoa(buf, day, 2)
|
||||
*buf = append(*buf, ' ')
|
||||
hour, min, sec := t.Clock()
|
||||
itoa(buf, hour, 2)
|
||||
*buf = append(*buf, ':')
|
||||
itoa(buf, min, 2)
|
||||
*buf = append(*buf, ':')
|
||||
itoa(buf, sec, 2)
|
||||
*buf = append(*buf, ' ')
|
||||
*buf = append(*buf, '[')
|
||||
*buf = append(*buf, getLevelText(level)...)
|
||||
*buf = append(*buf, ']')
|
||||
*buf = append(*buf, ' ')
|
||||
*buf = append(*buf, s...)
|
||||
return
|
||||
}
|
||||
|
||||
// Write 实现标准的写入行数
|
||||
func (lg *File) Write(p []byte) (n int, err error) {
|
||||
lg.mutex.Lock()
|
||||
defer lg.mutex.Unlock()
|
||||
if n, err = lg.fp.Write(p); err != nil {
|
||||
return
|
||||
}
|
||||
lg.size += int64(n)
|
||||
if lg.MaxSize > 0 && lg.size >= lg.MaxSize {
|
||||
if err = lg.rotate(); err != nil {
|
||||
return
|
||||
}
|
||||
lg.size = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (lg *File) write(level int, s string) {
|
||||
var (
|
||||
n int
|
||||
err error
|
||||
)
|
||||
if lg.Level > level {
|
||||
return
|
||||
}
|
||||
lg.mutex.Lock()
|
||||
defer lg.mutex.Unlock()
|
||||
lg.buf = lg.buf[:0]
|
||||
if err = lg.format(&lg.buf, level, s); err != nil {
|
||||
return
|
||||
}
|
||||
lg.buf = append(lg.buf, '\n')
|
||||
if n, err = lg.fp.Write(lg.buf); err != nil {
|
||||
return
|
||||
}
|
||||
lg.size += int64(n)
|
||||
if lg.MaxSize > 0 && lg.size >= lg.MaxSize {
|
||||
if err = lg.rotate(); err != nil {
|
||||
return
|
||||
}
|
||||
lg.size = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (lg *File) isExists(filename string) bool {
|
||||
if _, err := os.Stat(filename); err == nil {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// rotate 实现日志滚动处理
|
||||
func (lg *File) rotate() (err error) {
|
||||
if err = lg.close(); err != nil {
|
||||
return
|
||||
}
|
||||
for i := lg.MaxLogFiles; i >= 0; i-- {
|
||||
filename := lg.Filename
|
||||
if i > 0 {
|
||||
filename += "." + strconv.Itoa(i)
|
||||
}
|
||||
if i == lg.MaxLogFiles {
|
||||
if lg.isExists(filename) {
|
||||
if err = os.Remove(filename); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if lg.isExists(filename) {
|
||||
if err = os.Rename(filename, lg.Filename+"."+strconv.Itoa(i+1)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
err = lg.open()
|
||||
return
|
||||
}
|
||||
|
||||
func (lg *File) Reload() (err error) {
|
||||
lg.mutex.Lock()
|
||||
defer lg.mutex.Unlock()
|
||||
_ = lg.close()
|
||||
err = lg.open()
|
||||
return
|
||||
}
|
||||
|
||||
func (lg *File) open() (err error) {
|
||||
if lg.fp, err = os.OpenFile(lg.Filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (lg *File) Open() (err error) {
|
||||
var (
|
||||
info os.FileInfo
|
||||
)
|
||||
if err = lg.open(); err != nil {
|
||||
return
|
||||
}
|
||||
if info, err = os.Stat(lg.Filename); err == nil {
|
||||
lg.size = info.Size()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (lg *File) close() (err error) {
|
||||
if lg.fp != nil {
|
||||
err = lg.fp.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (lg *File) Close() (err error) {
|
||||
err = lg.close()
|
||||
return
|
||||
}
|
||||
|
||||
func NewFileLogger(filename string) *File {
|
||||
lg := &File{Filename: filename, buf: make([]byte, 1024)}
|
||||
return lg
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package log
|
||||
|
||||
var (
|
||||
std Logger
|
||||
)
|
||||
|
||||
func init() {
|
||||
std = NewConsoleLogger()
|
||||
}
|
||||
|
||||
func SetLogger(l Logger) {
|
||||
std = l
|
||||
}
|
||||
|
||||
func GetLogger() Logger {
|
||||
return std
|
||||
}
|
||||
|
||||
func Prefix(s string) {
|
||||
std.Prefix(s)
|
||||
}
|
||||
|
||||
func SetLevel(lv int) {
|
||||
std.SetLevel(lv)
|
||||
}
|
||||
|
||||
func Print(args ...interface{}) {
|
||||
std.Print(args...)
|
||||
}
|
||||
|
||||
func Printf(format string, args ...interface{}) {
|
||||
std.Printf(format, args...)
|
||||
}
|
||||
|
||||
func Debug(args ...interface{}) {
|
||||
std.Debug(args...)
|
||||
}
|
||||
|
||||
func Debugf(format string, args ...interface{}) {
|
||||
std.Debugf(format, args...)
|
||||
}
|
||||
|
||||
func Info(args ...interface{}) {
|
||||
std.Info(args...)
|
||||
}
|
||||
|
||||
func Infof(format string, args ...interface{}) {
|
||||
std.Infof(format, args...)
|
||||
}
|
||||
|
||||
func Warn(args ...interface{}) {
|
||||
std.Warn(args...)
|
||||
}
|
||||
|
||||
func Warnf(format string, args ...interface{}) {
|
||||
std.Warnf(format, args...)
|
||||
}
|
||||
|
||||
func Error(args ...interface{}) {
|
||||
std.Error(args...)
|
||||
}
|
||||
|
||||
func Errorf(format string, args ...interface{}) {
|
||||
std.Errorf(format, args...)
|
||||
}
|
||||
|
||||
func Fatal(args ...interface{}) {
|
||||
std.Fatal(args...)
|
||||
}
|
||||
|
||||
func Fatalf(format string, args ...interface{}) {
|
||||
std.Fatalf(format, args...)
|
||||
}
|
||||
|
||||
func Panic(args ...interface{}) {
|
||||
std.Panic(args...)
|
||||
}
|
||||
|
||||
func Panicf(format string, args ...interface{}) {
|
||||
std.Panicf(format, args...)
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package log
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
TraceLevel int = iota + 1
|
||||
DebugLevel
|
||||
InfoLevel
|
||||
WarnLevel
|
||||
ErrorLevel
|
||||
FatalLevel
|
||||
PanicLevel
|
||||
)
|
||||
|
||||
var (
|
||||
lvText = map[int]string{
|
||||
TraceLevel: "TRACE",
|
||||
DebugLevel: "DEBUG",
|
||||
InfoLevel: "INFO",
|
||||
WarnLevel: "WARN",
|
||||
ErrorLevel: "ERROR",
|
||||
FatalLevel: "FATAL",
|
||||
PanicLevel: "PANIC",
|
||||
}
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
SetLevel(int)
|
||||
Prefix(string)
|
||||
Print(i ...interface{})
|
||||
Printf(format string, args ...interface{})
|
||||
Debug(i ...interface{})
|
||||
Debugf(format string, args ...interface{})
|
||||
Info(i ...interface{})
|
||||
Infof(format string, args ...interface{})
|
||||
Warn(i ...interface{})
|
||||
Warnf(format string, args ...interface{})
|
||||
Error(i ...interface{})
|
||||
Errorf(format string, args ...interface{})
|
||||
Fatal(i ...interface{})
|
||||
Fatalf(format string, args ...interface{})
|
||||
Panic(i ...interface{})
|
||||
Panicf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
func getLevelText(lv int) string {
|
||||
if s, ok := lvText[lv]; ok {
|
||||
return s
|
||||
} else {
|
||||
return "TRACE"
|
||||
}
|
||||
}
|
||||
|
||||
func FormatLevel(lv int) string {
|
||||
return getLevelText(lv)
|
||||
}
|
||||
|
||||
func ParseLevel(s string) (lv int) {
|
||||
lv = DebugLevel
|
||||
s = strings.ToUpper(s)
|
||||
for k, v := range lvText {
|
||||
if s == v {
|
||||
lv = k
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseLevelWithoutDefault(s string) (lv int) {
|
||||
lv = -1
|
||||
s = strings.ToUpper(s)
|
||||
for k, v := range lvText {
|
||||
if s == v {
|
||||
lv = k
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package kos
|
||||
|
||||
import "context"
|
||||
|
||||
type Plugin interface {
|
||||
Name() string
|
||||
Mount(ctx context.Context) (err error)
|
||||
BeforeStart() (err error)
|
||||
AfterStart() (err error)
|
||||
BeforeStop() (err error)
|
||||
AfterStop() (err error)
|
||||
Umount() (err error)
|
||||
}
|
|
@ -0,0 +1,347 @@
|
|||
package kos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrStopping = errors.New("stopping")
|
||||
|
||||
cliFlag = flag.Bool("cli", false, "Go application interactive mode")
|
||||
)
|
||||
|
||||
type (
|
||||
application struct {
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelCauseFunc
|
||||
opts *Options
|
||||
gateway *entry.Gateway
|
||||
http *http.Server
|
||||
command *cli.Server
|
||||
uptime time.Time
|
||||
info *Info
|
||||
plugins sync.Map
|
||||
waitGroup conc.WaitGroup
|
||||
exitFlag int32
|
||||
}
|
||||
)
|
||||
|
||||
func (app *application) Log() log.Logger {
|
||||
return log.GetLogger()
|
||||
}
|
||||
|
||||
func (app *application) Healthy() string {
|
||||
if atomic.LoadInt32(&app.gateway.State().Processing) == 1 && atomic.LoadInt32(&app.gateway.State().Accepting) == 1 {
|
||||
return StateHealthy
|
||||
}
|
||||
if atomic.LoadInt32(&app.gateway.State().Processing) == 1 {
|
||||
return StateNoAccepting
|
||||
}
|
||||
if atomic.LoadInt32(&app.gateway.State().Accepting) == 1 {
|
||||
return StateNoProgress
|
||||
}
|
||||
return StateUnavailable
|
||||
}
|
||||
|
||||
func (app *application) Info() *Info {
|
||||
return app.info
|
||||
}
|
||||
|
||||
func (app *application) Http() *http.Server {
|
||||
return app.http
|
||||
}
|
||||
|
||||
func (app *application) Command() *cli.Server {
|
||||
return app.command
|
||||
}
|
||||
|
||||
func (app *application) Handle(path string, cb HandleFunc) {
|
||||
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) {
|
||||
return cb(ctx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
entry.Feature(http.MethodPost),
|
||||
entry.Feature(http.MethodPut),
|
||||
entry.Feature(http.MethodPatch),
|
||||
entry.Feature(http.MethodDelete),
|
||||
entry.Feature(http.MethodConnect),
|
||||
entry.Feature(http.MethodOptions),
|
||||
entry.Feature(http.MethodTrace),
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
if app.opts.EnableDebug {
|
||||
app.http.Handle(http.MethodGet, "/debug/pprof/", http.Wrap(pprof.Index))
|
||||
app.http.Handle(http.MethodGet, "/debug/pprof/goroutine", http.Wrap(pprof.Index))
|
||||
app.http.Handle(http.MethodGet, "/debug/pprof/heap", http.Wrap(pprof.Index))
|
||||
app.http.Handle(http.MethodGet, "/debug/pprof/mutex", http.Wrap(pprof.Index))
|
||||
app.http.Handle(http.MethodGet, "/debug/pprof/threadcreate", http.Wrap(pprof.Index))
|
||||
app.http.Handle(http.MethodGet, "/debug/pprof/cmdline", http.Wrap(pprof.Cmdline))
|
||||
app.http.Handle(http.MethodGet, "/debug/pprof/profile", http.Wrap(pprof.Profile))
|
||||
app.http.Handle(http.MethodGet, "/debug/pprof/symbol", http.Wrap(pprof.Symbol))
|
||||
app.http.Handle(http.MethodGet, "/debug/pprof/trace", http.Wrap(pprof.Trace))
|
||||
}
|
||||
timer := time.NewTimer(time.Millisecond * 200)
|
||||
defer timer.Stop()
|
||||
errChan := make(chan error, 1)
|
||||
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")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
timer := time.NewTimer(time.Millisecond * 200)
|
||||
defer timer.Stop()
|
||||
errChan := make(chan error, 1)
|
||||
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")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (app *application) gotoInteractive() (err error) {
|
||||
var (
|
||||
client *cli.Client
|
||||
)
|
||||
client = cli.NewClient(
|
||||
app.ctx,
|
||||
net.JoinHostPort(app.opts.Address, strconv.Itoa(app.opts.Port)),
|
||||
)
|
||||
return client.Shell()
|
||||
}
|
||||
|
||||
func (app *application) buildInfo() *Info {
|
||||
info := &Info{
|
||||
ID: app.opts.Name,
|
||||
Name: app.opts.Name,
|
||||
Version: app.opts.Version,
|
||||
Status: StateHealthy,
|
||||
Address: app.opts.Address,
|
||||
Port: app.opts.Port,
|
||||
Metadata: app.opts.Metadata,
|
||||
}
|
||||
if info.Metadata == nil {
|
||||
info.Metadata = make(map[string]string)
|
||||
}
|
||||
info.Metadata["os"] = runtime.GOOS
|
||||
info.Metadata["numOfCPU"] = strconv.Itoa(runtime.NumCPU())
|
||||
info.Metadata["goVersion"] = runtime.Version()
|
||||
info.Metadata["shortName"] = app.opts.ShortName()
|
||||
info.Metadata["upTime"] = app.uptime.Format(time.DateTime)
|
||||
return info
|
||||
}
|
||||
|
||||
func (app *application) preStart() (err error) {
|
||||
var (
|
||||
addr string
|
||||
)
|
||||
app.ctx, app.cancelFunc = context.WithCancelCause(app.opts.Context)
|
||||
if *cliFlag && !app.opts.DisableCommand {
|
||||
if err = app.gotoInteractive(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
app.info = app.buildInfo()
|
||||
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 {
|
||||
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 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if app.opts.server != nil {
|
||||
if err = app.opts.server.Start(app.ctx); err != nil {
|
||||
app.Log().Warnf("server start error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if !app.opts.DisableStateApi {
|
||||
app.Handle("/-/run/state", func(ctx Context) (err error) {
|
||||
return ctx.Success(State{
|
||||
ID: app.opts.Name,
|
||||
Name: app.opts.Name,
|
||||
Version: app.opts.Version,
|
||||
Uptime: time.Now().Sub(app.uptime).String(),
|
||||
Gateway: app.gateway.State(),
|
||||
})
|
||||
})
|
||||
app.Handle("/-/healthy", func(ctx Context) (err error) {
|
||||
return ctx.Success(app.Healthy())
|
||||
})
|
||||
}
|
||||
app.plugins.Range(func(key, value any) bool {
|
||||
if plugin, ok := value.(Plugin); ok {
|
||||
if err = plugin.AfterStart(); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
app.Log().Infof("server started")
|
||||
return
|
||||
}
|
||||
|
||||
func (app *application) preStop() (err error) {
|
||||
if !atomic.CompareAndSwapInt32(&app.exitFlag, 0, 1) {
|
||||
return
|
||||
}
|
||||
app.Log().Infof("server stopping")
|
||||
app.cancelFunc(ErrStopping)
|
||||
app.plugins.Range(func(key, value any) bool {
|
||||
if plugin, ok := value.(Plugin); ok {
|
||||
if err = plugin.BeforeStop(); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
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.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())
|
||||
}
|
||||
app.plugins.Range(func(key, value any) bool {
|
||||
if plugin, ok := value.(Plugin); ok {
|
||||
if err = plugin.AfterStop(); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
app.waitGroup.Wait()
|
||||
app.Log().Infof("server stopped")
|
||||
return
|
||||
}
|
||||
|
||||
func (app *application) Use(plugin Plugin) (err error) {
|
||||
var (
|
||||
ok bool
|
||||
)
|
||||
if _, ok = app.plugins.Load(plugin.Name()); ok {
|
||||
return fmt.Errorf("plugin %s already registered", plugin.Name())
|
||||
}
|
||||
if err = plugin.Mount(app.ctx); err != nil {
|
||||
return
|
||||
}
|
||||
app.plugins.Store(plugin.Name(), plugin)
|
||||
return
|
||||
}
|
||||
|
||||
func (app *application) Run() (err error) {
|
||||
if err = app.preStart(); err != nil {
|
||||
return
|
||||
}
|
||||
ch := make(chan os.Signal, 1)
|
||||
if app.opts.Signals == nil {
|
||||
app.opts.Signals = []os.Signal{syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL}
|
||||
}
|
||||
signal.Notify(ch, app.opts.Signals...)
|
||||
select {
|
||||
case <-ch:
|
||||
case <-app.ctx.Done():
|
||||
}
|
||||
return app.preStop()
|
||||
}
|
||||
|
||||
func New(cbs ...Option) *application {
|
||||
opts := NewOptions()
|
||||
for _, cb := range cbs {
|
||||
cb(opts)
|
||||
}
|
||||
app := &application{
|
||||
opts: opts,
|
||||
uptime: time.Now(),
|
||||
}
|
||||
return app
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package kos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.nspix.com/golang/kos/entry"
|
||||
"git.nspix.com/golang/kos/entry/cli"
|
||||
"git.nspix.com/golang/kos/entry/http"
|
||||
)
|
||||
|
||||
type (
|
||||
// Server custom attach server
|
||||
Server interface {
|
||||
Start(ctx context.Context) (err error)
|
||||
Stop() (err error)
|
||||
}
|
||||
|
||||
// HandleFunc request handle func
|
||||
HandleFunc func(ctx Context) (err error)
|
||||
|
||||
// Application app interface
|
||||
Application interface {
|
||||
Info() *Info
|
||||
Http() *http.Server
|
||||
Command() *cli.Server
|
||||
Handle(method string, cb HandleFunc)
|
||||
}
|
||||
|
||||
// Info application information
|
||||
Info struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
VcsVersion string `json:"vcsVersion"`
|
||||
Status string `json:"status"`
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
// State application runtime state
|
||||
State struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Uptime string `json:"uptime"`
|
||||
Gateway *entry.State `json:"gateway"`
|
||||
}
|
||||
)
|
|
@ -0,0 +1,68 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
|
||||
m := blockSize - len(ciphertext)%blockSize
|
||||
n := bytes.Repeat([]byte{byte(m)}, m)
|
||||
return append(ciphertext, n...)
|
||||
}
|
||||
|
||||
func PKCS7UnPadding(origData []byte) []byte {
|
||||
m := len(origData)
|
||||
n := int(origData[m-1])
|
||||
if m > n {
|
||||
return origData[:(m - n)]
|
||||
} else {
|
||||
return origData
|
||||
}
|
||||
}
|
||||
|
||||
func Encrypt(buf, key []byte) ([]byte, error) {
|
||||
var (
|
||||
err error
|
||||
blockSize int
|
||||
block cipher.Block
|
||||
)
|
||||
if block, err = aes.NewCipher(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
err = fmt.Errorf("decrypt error %v", v)
|
||||
}
|
||||
}()
|
||||
blockSize = block.BlockSize()
|
||||
buf = PKCS7Padding(buf, blockSize)
|
||||
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
|
||||
tmp := make([]byte, len(buf))
|
||||
blockMode.CryptBlocks(tmp, buf)
|
||||
return tmp, nil
|
||||
}
|
||||
|
||||
func Decrypt(buf, key []byte) ([]byte, error) {
|
||||
var (
|
||||
err error
|
||||
blockSize int
|
||||
block cipher.Block
|
||||
)
|
||||
if block, err = aes.NewCipher(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
err = fmt.Errorf("decrypt error %v", v)
|
||||
}
|
||||
}()
|
||||
blockSize = block.BlockSize()
|
||||
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
|
||||
origData := make([]byte, len(buf))
|
||||
blockMode.CryptBlocks(origData, buf)
|
||||
origData = PKCS7UnPadding(origData)
|
||||
return origData, nil
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package env
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Get(name string, val string) string {
|
||||
value := strings.TrimSpace(os.Getenv(name))
|
||||
if value == "" {
|
||||
return val
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func Integer(name string, val int64) int64 {
|
||||
value := Get(name, "")
|
||||
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 {
|
||||
return n
|
||||
} else {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
func Set(name string, val string) {
|
||||
value := os.Getenv(name)
|
||||
if value == "" {
|
||||
os.Setenv(name, val)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package fetch
|
||||
|
||||
const (
|
||||
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36 Edg/104.0.1293.54"
|
||||
)
|
||||
|
||||
const (
|
||||
JSON = "application/json"
|
||||
XML = "application/xml"
|
||||
)
|
|
@ -0,0 +1,177 @@
|
|||
package fetch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
httpClient = http.Client{
|
||||
Timeout: time.Second * 15,
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
ForceAttemptHTTP2: false,
|
||||
MaxIdleConns: 10,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func Get(ctx context.Context, urlString string, cbs ...Option) (res *http.Response, err error) {
|
||||
var (
|
||||
uri *url.URL
|
||||
req *http.Request
|
||||
)
|
||||
opts := newOptions()
|
||||
for _, cb := range cbs {
|
||||
cb(opts)
|
||||
}
|
||||
if uri, err = url.Parse(urlString); err != nil {
|
||||
return
|
||||
}
|
||||
if opts.Params != nil {
|
||||
qs := uri.Query()
|
||||
for k, v := range opts.Params {
|
||||
qs.Set(k, v)
|
||||
}
|
||||
uri.RawQuery = qs.Encode()
|
||||
}
|
||||
if req, err = http.NewRequest(http.MethodGet, uri.String(), nil); err != nil {
|
||||
return
|
||||
}
|
||||
if opts.Header != nil {
|
||||
for k, v := range opts.Header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
return Do(ctx, req)
|
||||
}
|
||||
|
||||
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
|
||||
reader io.Reader
|
||||
)
|
||||
opts := newOptions()
|
||||
for _, cb := range cbs {
|
||||
cb(opts)
|
||||
}
|
||||
if uri, err = url.Parse(urlString); err != nil {
|
||||
return
|
||||
}
|
||||
if opts.Params != nil {
|
||||
qs := uri.Query()
|
||||
for k, v := range opts.Params {
|
||||
qs.Set(k, v)
|
||||
}
|
||||
uri.RawQuery = qs.Encode()
|
||||
}
|
||||
if opts.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 req, err = http.NewRequest(http.MethodPost, uri.String(), reader); err != nil {
|
||||
return
|
||||
}
|
||||
if opts.Header != nil {
|
||||
for k, v := range opts.Header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
return Do(ctx, req)
|
||||
}
|
||||
|
||||
func Do(ctx context.Context, req *http.Request) (res *http.Response, err error) {
|
||||
return httpClient.Do(req.WithContext(ctx))
|
||||
}
|
||||
|
||||
func Request(ctx context.Context, urlString string, response any, cbs ...Option) (err error) {
|
||||
var (
|
||||
buf []byte
|
||||
uri *url.URL
|
||||
res *http.Response
|
||||
req *http.Request
|
||||
contentType string
|
||||
)
|
||||
opts := newOptions()
|
||||
for _, cb := range cbs {
|
||||
cb(opts)
|
||||
}
|
||||
if uri, err = url.Parse(urlString); err != nil {
|
||||
return
|
||||
}
|
||||
if opts.Params != nil {
|
||||
qs := uri.Query()
|
||||
for k, v := range opts.Params {
|
||||
qs.Set(k, v)
|
||||
}
|
||||
uri.RawQuery = qs.Encode()
|
||||
}
|
||||
if req, err = http.NewRequest(http.MethodGet, uri.String(), nil); err != nil {
|
||||
return
|
||||
}
|
||||
if opts.Header != nil {
|
||||
for k, v := range opts.Header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
if res, err = Do(ctx, req); 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)
|
||||
}
|
||||
return
|
||||
}
|
||||
contentType = strings.ToLower(res.Header.Get("Content-Type"))
|
||||
if strings.Contains(contentType, JSON) {
|
||||
err = json.NewDecoder(res.Body).Decode(response)
|
||||
} else if strings.Contains(contentType, XML) {
|
||||
err = xml.NewDecoder(res.Body).Decode(response)
|
||||
} else {
|
||||
err = fmt.Errorf("unsupported content type: %s", contentType)
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package fetch
|
||||
|
||||
import "net/http"
|
||||
|
||||
type (
|
||||
Options struct {
|
||||
Url string
|
||||
Method string
|
||||
Header map[string]string
|
||||
Params map[string]string
|
||||
Data any
|
||||
}
|
||||
Option func(o *Options)
|
||||
)
|
||||
|
||||
func WithUrl(s string) Option {
|
||||
return func(o *Options) {
|
||||
o.Url = s
|
||||
}
|
||||
}
|
||||
|
||||
func WithMethod(s string) Option {
|
||||
return func(o *Options) {
|
||||
o.Method = s
|
||||
}
|
||||
}
|
||||
|
||||
func WithHeader(h map[string]string) Option {
|
||||
return func(o *Options) {
|
||||
o.Header = h
|
||||
}
|
||||
}
|
||||
|
||||
func WithParams(h map[string]string) Option {
|
||||
return func(o *Options) {
|
||||
o.Params = h
|
||||
}
|
||||
}
|
||||
|
||||
func WithData(v any) Option {
|
||||
return func(o *Options) {
|
||||
o.Data = v
|
||||
}
|
||||
}
|
||||
|
||||
func newOptions() *Options {
|
||||
return &Options{
|
||||
Method: http.MethodGet,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
// IsDir Tells whether the filename is a directory
|
||||
func IsDir(filename string) (bool, error) {
|
||||
fd, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
fm := fd.Mode()
|
||||
return fm.IsDir(), nil
|
||||
}
|
||||
|
||||
// DirectoryOrCreate checking directory, is not exists will create
|
||||
func DirectoryOrCreate(dirname string) error {
|
||||
if fi, err := os.Stat(dirname); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return os.MkdirAll(dirname, 0755)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return errors.New("file not directory")
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package fs
|
|
@ -0,0 +1,44 @@
|
|||
package ip
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func External() (res []string) {
|
||||
var (
|
||||
err error
|
||||
addrs []net.Addr
|
||||
inters []net.Interface
|
||||
)
|
||||
if inters, err = net.Interfaces(); err != nil {
|
||||
return
|
||||
}
|
||||
for _, inter := range inters {
|
||||
if !strings.HasPrefix(inter.Name, "lo") {
|
||||
if addrs, err = inter.Addrs(); err != nil {
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ipNet, ok := addr.(*net.IPNet); ok {
|
||||
if ipNet.IP.IsLoopback() || ipNet.IP.IsLinkLocalMulticast() || ipNet.IP.IsLinkLocalUnicast() {
|
||||
continue
|
||||
}
|
||||
if ip4 := ipNet.IP.To4(); ip4 != nil {
|
||||
switch true {
|
||||
case ip4[0] == 10:
|
||||
continue
|
||||
case ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31:
|
||||
continue
|
||||
case ip4[0] == 192 && ip4[1] == 168:
|
||||
continue
|
||||
default:
|
||||
res = append(res, ipNet.IP.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package ip
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Internal get internal ip.
|
||||
func Internal() string {
|
||||
var (
|
||||
err error
|
||||
addrs []net.Addr
|
||||
inters []net.Interface
|
||||
)
|
||||
if inters, err = net.Interfaces(); err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, inter := range inters {
|
||||
if !isUp(inter.Flags) {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(inter.Name, "lo") {
|
||||
if addrs, err = inter.Addrs(); err != nil {
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
||||
if ipNet.IP.To4() != nil {
|
||||
return ipNet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package ip
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
)
|
||||
|
||||
// isUp Interface is up
|
||||
func isUp(v net.Flags) bool {
|
||||
return v&net.FlagUp == net.FlagUp
|
||||
}
|
||||
|
||||
// ToLong Converts a string containing an (IPv4) Internet Protocol dotted address into a long integer
|
||||
func ToLong(ipAddress string) uint32 {
|
||||
ip := net.ParseIP(ipAddress)
|
||||
if ip == nil {
|
||||
return 0
|
||||
}
|
||||
return binary.BigEndian.Uint32(ip.To4())
|
||||
}
|
||||
|
||||
// FromLong Converts a long integer address into a string in (IPv4) Internet standard dotted format
|
||||
func FromLong(properAddress uint32) string {
|
||||
ipByte := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(ipByte, properAddress)
|
||||
ip := net.IP(ipByte)
|
||||
return ip.String()
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package pool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
bufferPool sync.Pool
|
||||
)
|
||||
|
||||
func GetBuffer() *bytes.Buffer {
|
||||
if v := bufferPool.Get(); v != nil {
|
||||
return v.(*bytes.Buffer)
|
||||
}
|
||||
return bytes.NewBuffer([]byte{})
|
||||
}
|
||||
|
||||
func PutBuffer(b *bytes.Buffer) {
|
||||
b.Reset()
|
||||
bufferPool.Put(b)
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package pool
|
||||
|
||||
import "sync"
|
||||
|
||||
var (
|
||||
bufPool5k sync.Pool
|
||||
bufPool2k sync.Pool
|
||||
bufPool1k sync.Pool
|
||||
bufPool sync.Pool
|
||||
)
|
||||
|
||||
func GetBytes(size int) []byte {
|
||||
if size <= 0 {
|
||||
return nil
|
||||
}
|
||||
var x interface{}
|
||||
if size >= 5*1024 {
|
||||
x = bufPool5k.Get()
|
||||
} else if size >= 2*1024 {
|
||||
x = bufPool2k.Get()
|
||||
} else if size >= 1*1024 {
|
||||
x = bufPool1k.Get()
|
||||
} else {
|
||||
x = bufPool.Get()
|
||||
}
|
||||
if x == nil {
|
||||
return make([]byte, size)
|
||||
}
|
||||
buf := x.([]byte)
|
||||
if cap(buf) < size {
|
||||
return make([]byte, size)
|
||||
}
|
||||
return buf[:size]
|
||||
}
|
||||
|
||||
func PutBytes(buf []byte) {
|
||||
size := cap(buf)
|
||||
if size <= 0 {
|
||||
return
|
||||
}
|
||||
if size >= 5*1024 {
|
||||
bufPool5k.Put(buf)
|
||||
} else if size >= 2*1024 {
|
||||
bufPool2k.Put(buf)
|
||||
} else if size >= 1*1024 {
|
||||
bufPool1k.Put(buf)
|
||||
} else {
|
||||
bufPool.Put(buf)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package random
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
func Int(min, max int64) int64 {
|
||||
return min + rand.Int63n(max-min)
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,28 @@
|
|||
package random
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
Uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
Lowercase = "abcdefghijklmnopqrstuvwxyz"
|
||||
Alphabetic = Uppercase + Lowercase
|
||||
Numeric = "0123456789"
|
||||
Alphanumeric = Alphabetic + Numeric
|
||||
Symbols = "`" + `~!@#$%^&*()-_+={}[]|\;:"<>,./?`
|
||||
Hex = Numeric + "abcdef"
|
||||
)
|
||||
|
||||
func String(length uint8, charsets ...string) string {
|
||||
charset := strings.Join(charsets, "")
|
||||
if charset == "" {
|
||||
charset = Alphanumeric
|
||||
}
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[rand.Int63()%int64(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
package reflect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return refValue
|
||||
}
|
||||
|
||||
func safeAssignment(variable reflect.Value, value interface{}) (err error) {
|
||||
var (
|
||||
n int64
|
||||
un uint64
|
||||
fn float64
|
||||
kind reflect.Kind
|
||||
)
|
||||
rv := reflect.ValueOf(value)
|
||||
kind = variable.Kind()
|
||||
if kind != reflect.Slice && kind != reflect.Array && kind != reflect.Map && kind == rv.Kind() {
|
||||
variable.Set(rv)
|
||||
return
|
||||
}
|
||||
switch kind {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
switch rv.Kind() {
|
||||
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:
|
||||
variable.SetInt(int64(rv.Uint()))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
variable.SetInt(int64(rv.Float()))
|
||||
case reflect.String:
|
||||
if n, err = strconv.ParseInt(rv.String(), 10, 64); err == nil {
|
||||
variable.SetInt(n)
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("unsupported kind %s", rv.Kind())
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
switch rv.Kind() {
|
||||
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:
|
||||
variable.SetUint(rv.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
variable.SetUint(uint64(rv.Float()))
|
||||
case reflect.String:
|
||||
if un, err = strconv.ParseUint(rv.String(), 10, 64); err == nil {
|
||||
variable.SetUint(un)
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("unsupported kind %s", rv.Kind())
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
switch rv.Kind() {
|
||||
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:
|
||||
variable.SetFloat(float64(rv.Uint()))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
variable.SetFloat(rv.Float())
|
||||
case reflect.String:
|
||||
if fn, err = strconv.ParseFloat(rv.String(), 64); err == nil {
|
||||
variable.SetFloat(fn)
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("unsupported kind %s", rv.Kind())
|
||||
}
|
||||
case reflect.String:
|
||||
switch rv.Kind() {
|
||||
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:
|
||||
variable.SetString(strconv.FormatUint(rv.Uint(), 10))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
variable.SetString(strconv.FormatFloat(rv.Float(), 'f', -1, 64))
|
||||
case reflect.String:
|
||||
variable.SetString(rv.String())
|
||||
default:
|
||||
variable.SetString(fmt.Sprint(value))
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("unsupported kind %s", kind)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Set(hacky interface{}, field string, value interface{}) (err error) {
|
||||
var (
|
||||
n int
|
||||
refField reflect.Value
|
||||
)
|
||||
refVal := reflect.ValueOf(hacky)
|
||||
if refVal.Kind() == reflect.Ptr {
|
||||
refVal = reflect.Indirect(refVal)
|
||||
}
|
||||
if refVal.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("%s kind is %v", refVal.Type().String(), refField.Kind())
|
||||
}
|
||||
refField = findField(refVal, field)
|
||||
if !refField.IsValid() {
|
||||
return fmt.Errorf("%s field `%s` not found", refVal.Type(), field)
|
||||
}
|
||||
rv := reflect.ValueOf(value)
|
||||
fieldKind := refField.Kind()
|
||||
if fieldKind != reflect.Slice && fieldKind != reflect.Array && fieldKind != reflect.Map && fieldKind == rv.Kind() {
|
||||
refField.Set(rv)
|
||||
return
|
||||
}
|
||||
switch fieldKind {
|
||||
case reflect.Array, reflect.Slice:
|
||||
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 {
|
||||
sliceVar.Index(n).Set(dstVal)
|
||||
n++
|
||||
}
|
||||
}
|
||||
refField.Set(sliceVar.Slice(0, n))
|
||||
}
|
||||
default:
|
||||
err = safeAssignment(refField, value)
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
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 h := os.Getenv("HOME"); h != "" {
|
||||
return h
|
||||
}
|
||||
return "/"
|
||||
}
|
||||
|
||||
// HiddenFile get hidden file prefix
|
||||
func HiddenFile(name string) string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return "~" + name
|
||||
default:
|
||||
return "." + name
|
||||
}
|
||||
}
|
||||
|
||||
// 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 xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
|
||||
return xdg
|
||||
}
|
||||
return filepath.Join(HomeDir(), ".cache")
|
||||
}
|
Loading…
Reference in New Issue