Compare commits

...

30 Commits

Author SHA1 Message Date
loveuer
c108679fc9 fix: loading print panic 2024-12-26 22:55:13 -08:00
loveuer
8235631d4f feat: nfctl(add version) 2024-12-26 19:51:31 -08:00
loveuer
ad6b4fe7b6 refc: 重构了 nfctl 2024-12-26 19:20:17 -08:00
loveuer
d8d771aec6 feat: allocate ctx by pool 2024-12-17 09:19:07 +08:00
loveuer
1e66a221e0 feat: add RenderHTML, Redirect 2024-10-25 10:23:52 +08:00
loveuer
df318682fa fix: nfctl new(project name include '/') 2024-09-22 20:50:19 -07:00
root
af1e58bce9 update: add resp 418 2024-09-19 01:43:05 -07:00
zhaoyupeng
940e86bd8d chore: update go module, readme 2024-08-27 14:39:24 +08:00
loveuer
5263cba44a chore: expose default logger
chore: add ctx alias method
2024-08-12 16:14:16 +08:00
loveuer
63f7516667 fix: c.Writer 2024-07-25 17:23:53 +08:00
loveuer
9b7f1e4413 chore: X-Trace-Id
fix req context trace id; default logger with trace-id

ci: fix nfctl run
2024-07-17 23:00:45 +08:00
loveuer
e4a6228b0a feat: add trace_id to context 2024-07-17 18:07:10 +08:00
loveuer
fb97d6e811 style: nfctl new version print 2024-07-14 21:17:40 +08:00
loveuer
f2a73056c8 fix: github action 2024-07-14 17:49:43 +08:00
loveuer
52ec9a0686 chore: nfctl version, check cmd 2024-07-14 17:26:48 +08:00
loveuer
cbb959a31e chore: add github workflow to build nfctl 2024-07-13 22:37:05 +08:00
loveuer
b267cc7a2e style: cmd.new add success tip 2024-07-12 16:33:35 +08:00
loveuer
0f139cda98 feat: add nfctl(ctl to start nf project) 2024-07-12 16:00:15 +08:00
loveuer
8a423c2887 dev: nft/ctl 2024-07-09 18:08:49 +08:00
loveuer
bf1c5ad92f feat: c.IP add use-proxy-header arg 2024-07-04 17:37:27 +08:00
loveuer
9b7c8d9d24 fix: writer 2024-06-25 16:36:43 +08:00
loveuer
c13263fe0d fix: logger missing status while 404 2024-06-17 14:31:49 +08:00
loveuer
d4fe4e0112 feat: refact default logger 2024-06-07 17:39:06 +08:00
loveuer
16541e377c fix: set status not work 2024-05-22 14:08:34 +08:00
loveuer
479c4eef57 feat: add ctx ssevent; fix: superfluous response.WriteHeader 2024-04-19 16:56:40 +08:00
loveuer
436264117c fix: ctx Status superfluous 2024-04-13 10:46:23 +08:00
loveuer
56fa3815cb update: hide Param, add SetParam(can't cover value) 2024-04-10 18:03:38 +08:00
loveuer
9530fa863f feat: ctx add SendStatus 2024-04-10 11:24:17 +08:00
loveuer
f3fb259eee feat: ctx add MultipartForm func 2024-03-11 16:28:33 +08:00
loveuer
67c15513a2 fix: ctx handlers out of length 2024-02-27 16:19:19 +08:00
45 changed files with 1539 additions and 610 deletions

62
.github/workflows/nfctl.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: Auto Build
on:
push:
branches:
- 'release/nfctl/*'
env:
RELEASE_VERSION: v24.09.23-r1
jobs:
build-job:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
repository-projects: write
steps:
- name: checkout repository
uses: actions/checkout@v4
- name: install golang
uses: actions/setup-go@v4
with:
go-version: '1.20'
- name: build linux amd64
run: CGO_ENABLE=0 GOOS=linux GOARCH=amd64 go build -ldflags='-s -w' -o "dist/nfctl-linux_amd64-${{ env.RELEASE_VERSION }}" nft/nfctl/main.go
- name: build linux arm64
run: CGO_ENABLE=0 GOOS=linux GOARCH=arm64 go build -ldflags='-s -w' -o "dist/nfctl-linux_arm64-${{ env.RELEASE_VERSION }}" nft/nfctl/main.go
- name: build windows amd64
run: CGO_ENABLE=0 GOOS=windows GOARCH=amd64 go build -ldflags='-s -w' -o "dist/nfctl-win_amd64-${{ env.RELEASE_VERSION }}.exe" nft/nfctl/main.go
- name: build windows arm64
run: CGO_ENABLE=0 GOOS=windows GOARCH=arm64 go build -ldflags='-s -w' -o "dist/nfctl-win_arm64-${{ env.RELEASE_VERSION }}.exe" nft/nfctl/main.go
- name: build darwin amd64
run: CGO_ENABLE=0 GOOS=darwin GOARCH=amd64 go build -ldflags='-s -w' -o "dist/nfctl-darwin_amd64-${{ env.RELEASE_VERSION }}" nft/nfctl/main.go
- name: build darwin arm64
run: CGO_ENABLE=0 GOOS=darwin GOARCH=arm64 go build -ldflags='-s -w' -o "dist/nfctl-darwin_arm64-${{ env.RELEASE_VERSION }}" nft/nfctl/main.go
- name: show all builds
run: ls -lash dist
- name: create releases
id: create_releases
uses: "marvinpinto/action-automatic-releases@latest"
with:
automatic_release_tag: "Release-nfctl-${{ env.RELEASE_VERSION }}"
repo_token: "${{ secrets.GITHUB_TOKEN }}"
title: "Release_${{ env.RELEASE_VERSION }}"
prerelease: false
files: |
dist/nfctl-linux_amd64-${{ env.RELEASE_VERSION }}
dist/nfctl-linux_arm64-${{ env.RELEASE_VERSION }}
dist/nfctl-win_amd64-${{ env.RELEASE_VERSION }}.exe
dist/nfctl-win_arm64-${{ env.RELEASE_VERSION }}.exe
dist/nfctl-darwin_amd64-${{ env.RELEASE_VERSION }}
dist/nfctl-darwin_arm64-${{ env.RELEASE_VERSION }}

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.idea
.vscode
.DS_Store
xtest

57
app.go
View File

@ -5,13 +5,15 @@ import (
"crypto/tls"
"errors"
"fmt"
"github.com/loveuer/nf/internal/bytesconv"
"io"
"log"
"net"
"net/http"
"path"
"regexp"
"sync"
"github.com/loveuer/nf/internal/bytesconv"
)
var (
@ -19,8 +21,6 @@ var (
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
mimePlain = []string{"text/plain"}
)
type App struct {
@ -31,6 +31,8 @@ type App struct {
trees methodTrees
pool *sync.Pool
maxParams uint16
maxSections uint16
@ -42,13 +44,34 @@ type App struct {
removeExtraSlash bool // false
}
func (a *App) allocateContext() *Ctx {
var (
skippedNodes = make([]skippedNode, 0, a.maxSections)
v = make(Params, 0, a.maxParams)
)
ctx := Ctx{
lock: sync.Mutex{},
app: a,
index: -1,
locals: make(map[string]any),
handlers: make([]HandlerFunc, 0),
skippedNodes: &skippedNodes,
params: &v,
}
return &ctx
}
func (a *App) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
var (
err error
c = newContext(a, writer, request)
c = a.pool.Get().(*Ctx)
nfe = new(Err)
)
c.reset(writer, request)
if err = c.verify(); err != nil {
if errors.As(err, nfe) {
_ = c.Status(nfe.Status).SendString(nfe.Msg)
@ -60,6 +83,8 @@ func (a *App) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
}
a.handleHTTPRequest(c)
a.pool.Put(c)
}
func (a *App) run(ln net.Listener) error {
@ -139,9 +164,7 @@ func (a *App) addRoute(method, path string, handlers ...HandlerFunc) {
}
func (a *App) handleHTTPRequest(c *Ctx) {
var (
err error
)
var err error
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
@ -165,8 +188,9 @@ func (a *App) handleHTTPRequest(c *Ctx) {
// Find route in tree
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
if value.params != nil {
c.Params = *value.params
c.params = value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
@ -175,7 +199,6 @@ func (a *App) handleHTTPRequest(c *Ctx) {
serveError(c, errorHandler)
}
c.writermem.WriteHeaderNow()
return
}
if httpMethod != http.MethodConnect && rPath != "/" {
@ -204,17 +227,19 @@ func (a *App) handleHTTPRequest(c *Ctx) {
}
if len(allowed) > 0 {
c.handlers = a.combineHandlers()
c.handlers = a.combineHandlers(a.config.MethodNotAllowedHandler)
serveError(c, a.config.MethodNotAllowedHandler)
_ = c.Next()
return
}
}
c.handlers = a.combineHandlers()
c.handlers = a.combineHandlers(a.config.NotFoundHandler)
serveError(c, a.config.NotFoundHandler)
_ = c.Next()
return
}
func errorHandler(c *Ctx) error {
@ -263,7 +288,7 @@ func redirectFixedPath(c *Ctx, root *node, trailingSlash bool) bool {
func redirectRequest(c *Ctx) {
req := c.Request
//rPath := req.URL.Path
// rPath := req.URL.Path
rURL := req.URL.String()
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
@ -271,8 +296,8 @@ func redirectRequest(c *Ctx) {
code = http.StatusTemporaryRedirect
}
//debugPrint("redirecting request %d: %s --> %s", code, rPath, rURL)
// debugPrint("redirecting request %d: %s --> %s", code, rPath, rURL)
http.Redirect(c.writer, req, rURL, code)
http.Redirect(c.Writer, req, rURL, code)
c.writermem.WriteHeaderNow()
}

191
ctx.go
View File

@ -2,24 +2,31 @@ package nf
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"mime/multipart"
"net"
"net/http"
"strings"
"sync"
"github.com/google/uuid"
"github.com/loveuer/nf/internal/sse"
)
var forwardHeaders = []string{"CF-Connecting-IP", "X-Forwarded-For", "X-Real-Ip"}
type Ctx struct {
writermem responseWriter
// origin objects
writer http.ResponseWriter
Request *http.Request
// request info
path string
method string
// response info
lock sync.Mutex
writermem responseWriter
Writer ResponseWriter
Request *http.Request
path string
method string
StatusCode int
app *App
@ -29,37 +36,31 @@ type Ctx struct {
locals map[string]interface{}
skippedNodes *[]skippedNode
fullPath string
Params Params
}
func newContext(app *App, writer http.ResponseWriter, request *http.Request) *Ctx {
skippedNodes := make([]skippedNode, 0, app.maxSections)
v := make(Params, 0, app.maxParams)
ctx := &Ctx{
writer: writer,
writermem: responseWriter{},
Request: request,
path: request.URL.Path,
method: request.Method,
StatusCode: 200,
app: app,
index: -1,
locals: map[string]interface{}{},
handlers: make([]HandlerFunc, 0),
skippedNodes: &skippedNodes,
params: &v,
func (c *Ctx) reset(w http.ResponseWriter, r *http.Request) {
traceId := r.Header.Get(TraceKey)
if traceId == "" {
traceId = uuid.Must(uuid.NewV7()).String()
}
ctx.writermem = responseWriter{
ResponseWriter: ctx.writer,
size: -1,
status: 0,
}
c.writermem.reset(w)
return ctx
c.Request = r.WithContext(context.WithValue(r.Context(), TraceKey, traceId))
c.Writer = &c.writermem
c.handlers = nil
c.index = -1
c.path = r.URL.Path
c.method = r.Method
c.StatusCode = 200
c.fullPath = ""
*c.params = (*c.params)[:0]
*c.skippedNodes = (*c.skippedNodes)[:0]
for key := range c.locals {
delete(c.locals, key)
}
c.writermem.Header().Set(TraceKey, traceId)
}
func (c *Ctx) Locals(key string, value ...interface{}) interface{} {
@ -91,9 +92,7 @@ func (c *Ctx) Path(overWrite ...string) string {
}
func (c *Ctx) Cookies(key string, defaultValue ...string) string {
var (
dv = ""
)
dv := ""
if len(defaultValue) > 0 {
dv = defaultValue[0]
@ -107,15 +106,22 @@ func (c *Ctx) Cookies(key string, defaultValue ...string) string {
return cookie.Value
}
func (c *Ctx) Context() context.Context {
return c.Request.Context()
}
func (c *Ctx) Next() error {
c.index++
if c.index >= len(c.handlers) {
return nil
}
var (
err error
handler = c.handlers[c.index]
)
//for c.index < len(c.handlers) {
if handler != nil {
if err = handler(c); err != nil {
return err
@ -123,7 +129,6 @@ func (c *Ctx) Next() error {
}
c.index++
//}
return nil
}
@ -142,18 +147,39 @@ func (c *Ctx) verify() error {
}
func (c *Ctx) Param(key string) string {
return c.Params.ByName(key)
return c.params.ByName(key)
}
func (c *Ctx) SetParam(key, value string) {
c.lock.Lock()
defer c.lock.Unlock()
params := append(*c.params, Param{Key: key, Value: value})
c.params = &params
}
func (c *Ctx) Form(key string) string {
return c.Request.FormValue(key)
}
// FormValue fiber ctx function
func (c *Ctx) FormValue(key string) string {
return c.Request.FormValue(key)
}
func (c *Ctx) FormFile(key string) (*multipart.FileHeader, error) {
_, fh, err := c.Request.FormFile(key)
return fh, err
}
func (c *Ctx) MultipartForm() (*multipart.Form, error) {
if err := c.Request.ParseMultipartForm(c.app.config.BodyLimit); err != nil {
return nil, err
}
return c.Request.MultipartForm, nil
}
func (c *Ctx) Query(key string) string {
return c.Request.URL.Query().Get(key)
}
@ -167,11 +193,24 @@ func (c *Ctx) Get(key string, defaultValue ...string) string {
return value
}
func (c *Ctx) IP() string {
func (c *Ctx) IP(useProxyHeader ...bool) string {
ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
if err != nil {
return ""
}
if len(useProxyHeader) > 0 && useProxyHeader[0] {
for _, h := range forwardHeaders {
for _, rip := range strings.Split(c.Request.Header.Get(h), ",") {
realIP := net.ParseIP(strings.Replace(rip, " ", "", -1))
if check := net.ParseIP(realIP.String()); check != nil {
ip = realIP.String()
break
}
}
}
}
return ip
}
@ -221,11 +260,6 @@ func (c *Ctx) BodyParser(out interface{}) error {
}
func (c *Ctx) QueryParser(out interface{}) error {
//v := reflect.ValueOf(out)
//
//if v.Kind() == reflect.Ptr && v.Elem().Kind() != reflect.Map {
//}
return parseToStruct("query", out, c.Request.URL.Query())
}
@ -234,17 +268,34 @@ func (c *Ctx) QueryParser(out interface{}) error {
=============================================================== */
func (c *Ctx) Status(code int) *Ctx {
c.writermem.WriteHeader(code)
c.lock.Lock()
defer c.lock.Unlock()
c.Writer.WriteHeader(code)
c.StatusCode = c.writermem.status
return c
}
// Set set response header
func (c *Ctx) Set(key string, value string) {
c.writermem.Header().Set(key, value)
c.Writer.Header().Set(key, value)
}
// AddHeader add response header
func (c *Ctx) AddHeader(key string, value string) {
c.Writer.Header().Add(key, value)
}
// SetHeader set response header
func (c *Ctx) SetHeader(key string, value string) {
c.writermem.Header().Set(key, value)
c.Writer.Header().Set(key, value)
}
func (c *Ctx) SendStatus(code int) error {
c.Status(code)
c.Writer.WriteHeaderNow()
return nil
}
func (c *Ctx) SendString(data string) error {
@ -255,13 +306,13 @@ func (c *Ctx) SendString(data string) error {
func (c *Ctx) Writef(format string, values ...interface{}) (int, error) {
c.SetHeader("Content-Type", "text/plain")
return c.writer.Write([]byte(fmt.Sprintf(format, values...)))
return c.Write([]byte(fmt.Sprintf(format, values...)))
}
func (c *Ctx) JSON(data interface{}) error {
c.SetHeader("Content-Type", MIMEApplicationJSON)
encoder := json.NewEncoder(&c.writermem)
encoder := json.NewEncoder(c.Writer)
if err := encoder.Encode(data); err != nil {
return err
@ -270,16 +321,44 @@ func (c *Ctx) JSON(data interface{}) error {
return nil
}
func (c *Ctx) RawWriter() http.ResponseWriter {
return c.writer
func (c *Ctx) SSEvent(event string, data interface{}) error {
c.Set("Content-Type", "text/event-stream")
c.Set("Cache-Control", "no-cache")
c.Set("Transfer-Encoding", "chunked")
return sse.Encode(c.Writer, sse.Event{Event: event, Data: data})
}
func (c *Ctx) Write(data []byte) (int, error) {
return c.writermem.Write(data)
func (c *Ctx) Flush() error {
if f, ok := c.Writer.(http.Flusher); ok {
f.Flush()
return nil
}
return errors.New("http.Flusher is not implemented")
}
func (c *Ctx) HTML(html string) error {
c.SetHeader("Content-Type", "text/html")
_, err := c.writer.Write([]byte(html))
_, err := c.Write([]byte(html))
return err
}
func (c *Ctx) RenderHTML(name, html string, obj any) error {
c.SetHeader("Content-Type", "text/html")
t, err := template.New(name).Parse(html)
if err != nil {
return err
}
return t.Execute(c.Writer, obj)
}
func (c *Ctx) Redirect(url string, code int) error {
http.Redirect(c.Writer, c.Request, url, code)
return nil
}
func (c *Ctx) Write(data []byte) (int, error) {
return c.Writer.Write(data)
}

36
go.mod
View File

@ -1,3 +1,39 @@
module github.com/loveuer/nf
go 1.20
require (
github.com/fatih/color v1.17.0
github.com/go-git/go-git/v5 v5.12.0
github.com/google/uuid v1.6.0
github.com/spf13/cobra v1.8.1
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-resty/resty/v2 v2.16.2
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/tools v0.13.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

151
go.sum
View File

@ -0,0 +1,151 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

106
internal/sse/sse-encoder.go Normal file
View File

@ -0,0 +1,106 @@
package sse
import (
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"strconv"
"strings"
)
// Server-Sent Events
// W3C Working Draft 29 October 2009
// http://www.w3.org/TR/2009/WD-eventsource-20091029/
const ContentType = "text/event-stream"
var contentType = []string{ContentType}
var noCache = []string{"no-cache"}
var fieldReplacer = strings.NewReplacer(
"\n", "\\n",
"\r", "\\r")
var dataReplacer = strings.NewReplacer(
"\n", "\ndata:",
"\r", "\\r")
type Event struct {
Event string
Id string
Retry uint
Data interface{}
}
func Encode(writer io.Writer, event Event) error {
w := checkWriter(writer)
writeId(w, event.Id)
writeEvent(w, event.Event)
writeRetry(w, event.Retry)
return writeData(w, event.Data)
}
func writeId(w stringWriter, id string) {
if len(id) > 0 {
w.WriteString("id:")
fieldReplacer.WriteString(w, id)
w.WriteString("\n")
}
}
func writeEvent(w stringWriter, event string) {
if len(event) > 0 {
w.WriteString("event:")
fieldReplacer.WriteString(w, event)
w.WriteString("\n")
}
}
func writeRetry(w stringWriter, retry uint) {
if retry > 0 {
w.WriteString("retry:")
w.WriteString(strconv.FormatUint(uint64(retry), 10))
w.WriteString("\n")
}
}
func writeData(w stringWriter, data interface{}) error {
w.WriteString("data:")
switch kindOfData(data) {
case reflect.Struct, reflect.Slice, reflect.Map:
err := json.NewEncoder(w).Encode(data)
if err != nil {
return err
}
w.WriteString("\n")
default:
dataReplacer.WriteString(w, fmt.Sprint(data))
w.WriteString("\n\n")
}
return nil
}
func (r Event) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
return Encode(w, r)
}
func (r Event) WriteContentType(w http.ResponseWriter) {
header := w.Header()
header["Content-Type"] = contentType
if _, exist := header["Cache-Control"]; !exist {
header["Cache-Control"] = noCache
}
}
func kindOfData(data interface{}) reflect.Kind {
value := reflect.ValueOf(data)
valueType := value.Kind()
if valueType == reflect.Ptr {
valueType = value.Elem().Kind()
}
return valueType
}

24
internal/sse/writer.go Normal file
View File

@ -0,0 +1,24 @@
package sse
import "io"
type stringWriter interface {
io.Writer
WriteString(string) (int, error)
}
type stringWrapper struct {
io.Writer
}
func (w stringWrapper) WriteString(str string) (int, error) {
return w.Writer.Write([]byte(str))
}
func checkWriter(writer io.Writer) stringWriter {
if w, ok := writer.(stringWriter); ok {
return w
} else {
return stringWrapper{writer}
}
}

View File

@ -2,10 +2,11 @@ package nf
import (
"fmt"
"log"
"os"
"runtime/debug"
"time"
"github.com/loveuer/nf/nft/log"
)
func NewRecover(enableStackTrace bool) HandlerFunc {
@ -18,7 +19,7 @@ func NewRecover(enableStackTrace bool) HandlerFunc {
os.Stderr.WriteString(fmt.Sprintf("recovered from panic: %v\n", r))
}
//serveError(c, 500, []byte(fmt.Sprint(r)))
// serveError(c, 500, []byte(fmt.Sprint(r)))
_ = c.Status(500).SendString(fmt.Sprint(r))
}
}()
@ -28,50 +29,28 @@ func NewRecover(enableStackTrace bool) HandlerFunc {
}
func NewLogger() HandlerFunc {
l := log.New(os.Stdout, "[NF] ", 0)
durationFormat := func(num int64) string {
var (
unit = "ns"
)
if num > 1000 {
num = num / 1000
unit = "µs"
}
if num > 1000 {
num = num / 1000
unit = "ms"
}
if num > 1000 {
num = num / 1000
unit = "s"
}
return fmt.Sprintf("%3d %2s", num, unit)
}
return func(c *Ctx) error {
start := time.Now()
var (
now = time.Now()
logFn func(msg string, data ...any)
ip = c.IP()
)
err := c.Next()
duration := time.Since(now)
var (
duration = time.Now().Sub(start).Nanoseconds()
status = c.StatusCode
path = c.path
method = c.Request.Method
)
msg := fmt.Sprintf("NF | %v | %15s | %3d | %s | %6s | %s", c.Context().Value(TraceKey), ip, c.StatusCode, HumanDuration(duration.Nanoseconds()), c.Method(), c.Path())
l.Printf("%s | %5s | %d | %s | %s",
start.Format("06/01/02T15:04:05"),
method,
status,
durationFormat(duration),
path,
)
switch {
case c.StatusCode >= 500:
logFn = log.Error
case c.StatusCode >= 400:
logFn = log.Warn
default:
logFn = log.Info
}
logFn(msg)
return err
}

47
nf.go
View File

@ -1,10 +1,13 @@
package nf
import "sync"
const (
banner = " _ _ _ ___ _ \n | \\| |___| |_ | __|__ _ _ _ _ __| |\n | .` / _ \\ _| | _/ _ \\ || | ' \\/ _` |\n |_|\\_\\___/\\__| |_|\\___/\\_,_|_||_\\__,_|\n "
_404 = "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1\"><meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"><title>Not Found</title><style>body{background:#333;margin:0;color:#ccc;display:flex;align-items:center;max-height:100vh;height:100vh;justify-content:center}textarea{min-height:5rem;min-width:20rem;text-align:center;border:none;background:0 0;color:#ccc;resize:none;user-input:none;user-select:none;cursor:default;-webkit-user-select:none;-webkit-touch-callout:none;-moz-user-select:none;-ms-user-select:none;outline:0}</style></head><body><textarea id=\"banner\" readonly=\"readonly\"></textarea><script type=\"text/javascript\">let htmlCodes = [\n ' _ _ _ ___ _ ',\n '| \\\\| |___| |_ | __|__ _ _ _ _ __| |',\n '| .` / _ \\\\ _| | _/ _ \\\\ || | \\' \\\\/ _` |',\n '|_|\\\\_\\\\___/\\\\__| |_|\\\\___/\\\\_,_|_||_\\\\__,_|'\n].join('\\n');\ndocument.querySelector('#banner').value = htmlCodes</script></body></html>"
_405 = `405 Method Not Allowed`
_500 = `500 Internal Server Error`
banner = " _ _ _ ___ _ \n | \\| |___| |_ | __|__ _ _ _ _ __| |\n | .` / _ \\ _| | _/ _ \\ || | ' \\/ _` |\n |_|\\_\\___/\\__| |_|\\___/\\_,_|_||_\\__,_|\n "
_404 = "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1\"><meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"><title>Not Found</title><style>body{background:#333;margin:0;color:#ccc;display:flex;align-items:center;max-height:100vh;height:100vh;justify-content:center}textarea{min-height:5rem;min-width:20rem;text-align:center;border:none;background:0 0;color:#ccc;resize:none;user-input:none;user-select:none;cursor:default;-webkit-user-select:none;-webkit-touch-callout:none;-moz-user-select:none;-ms-user-select:none;outline:0}</style></head><body><textarea id=\"banner\" readonly=\"readonly\"></textarea><script type=\"text/javascript\">let htmlCodes = [\n ' _ _ _ ___ _ ',\n '| \\\\| |___| |_ | __|__ _ _ _ _ __| |',\n '| .` / _ \\\\ _| | _/ _ \\\\ || | \\' \\\\/ _` |',\n '|_|\\\\_\\\\___/\\\\__| |_|\\\\___/\\\\_,_|_||_\\\\__,_|'\n].join('\\n');\ndocument.querySelector('#banner').value = htmlCodes</script></body></html>"
_405 = `405 Method Not Allowed`
_500 = `500 Internal Server Error`
TraceKey = "X-Trace-Id"
)
type Map map[string]interface{}
@ -22,26 +25,24 @@ type Config struct {
DisableRecover bool `json:"-"`
DisableHttpErrorLog bool `json:"-"`
//EnableNotImplementHandler bool `json:"-"`
// EnableNotImplementHandler bool `json:"-"`
NotFoundHandler HandlerFunc `json:"-"`
MethodNotAllowedHandler HandlerFunc `json:"-"`
}
var (
defaultConfig = &Config{
BodyLimit: 4 * 1024 * 1024,
NotFoundHandler: func(c *Ctx) error {
c.Set("Content-Type", MIMETextHTML)
_, err := c.Status(404).Write([]byte(_404))
return err
},
MethodNotAllowedHandler: func(c *Ctx) error {
c.Set("Content-Type", MIMETextPlain)
_, err := c.Status(405).Write([]byte(_405))
return err
},
}
)
var defaultConfig = &Config{
BodyLimit: 4 * 1024 * 1024,
NotFoundHandler: func(c *Ctx) error {
c.Set("Content-Type", MIMETextHTML)
_, err := c.Status(404).Write([]byte(_404))
return err
},
MethodNotAllowedHandler: func(c *Ctx) error {
c.Set("Content-Type", MIMETextPlain)
_, err := c.Status(405).Write([]byte(_405))
return err
},
}
func New(config ...Config) *App {
app := &App{
@ -51,6 +52,8 @@ func New(config ...Config) *App {
root: true,
},
pool: &sync.Pool{},
redirectTrailingSlash: true, // true
redirectFixedPath: false, // false
handleMethodNotAllowed: true, // false
@ -88,5 +91,9 @@ func New(config ...Config) *App {
app.Use(NewRecover(true))
}
app.pool.New = func() any {
return app.allocateContext()
}
return app
}

123
nft/loading/loading.go Normal file
View File

@ -0,0 +1,123 @@
package loading
import (
"context"
"fmt"
"time"
)
type Type int
const (
TypeProcessing Type = iota
TypeInfo
TypeSuccess
TypeWarning
TypeError
)
func (t Type) Symbol() string {
switch t {
case TypeSuccess:
return "✔️ "
case TypeWarning:
return "❗ "
case TypeError:
return "❌ "
case TypeInfo:
return "❕ "
default:
return ""
}
}
type _msg struct {
msg string
t Type
}
var frames = []string{"|", "/", "-", "\\"}
func Do(ctx context.Context, fn func(ctx context.Context, print func(msg string, types ...Type)) error) (err error) {
start := time.Now()
ch := make(chan *_msg)
go func() {
var (
m *_msg
ok bool
processing string
)
defer func() {
fmt.Printf("\r\033[K")
}()
for {
for _, frame := range frames {
select {
case <-ctx.Done():
return
case m, ok = <-ch:
if !ok || m == nil {
return
}
switch m.t {
case TypeProcessing:
if m.msg != "" {
processing = m.msg
}
case TypeInfo,
TypeSuccess,
TypeWarning,
TypeError:
// Clear the loading animation
fmt.Printf("\r\033[K")
fmt.Printf("%s%s\n", m.t.Symbol(), m.msg)
}
default:
elapsed := time.Since(start).Seconds()
if processing != "" {
fmt.Printf("\r\033[K%s %s (%.2fs)", frame, processing, elapsed)
}
time.Sleep(100 * time.Millisecond)
}
}
}
}()
printFn := func(msg string, types ...Type) {
if msg == "" {
return
}
m := &_msg{
msg: msg,
t: TypeProcessing,
}
if len(types) > 0 {
m.t = types[0]
}
ch <- m
}
done := make(chan struct{})
go func() {
if err = fn(ctx, printFn); err != nil {
ch <- &_msg{msg: err.Error(), t: TypeError}
}
close(ch)
done <- struct{}{}
}()
select {
case <-ctx.Done():
case <-done:
}
return err
}

View File

@ -0,0 +1,25 @@
package loading
import (
"context"
"os/signal"
"syscall"
"testing"
"time"
)
func TestLoadingPrint(t *testing.T) {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
defer cancel()
Do(ctx, func(ctx context.Context, print func(msg string, types ...Type)) error {
print("start task 1...")
time.Sleep(3 * time.Second)
print("warning...1", TypeWarning)
time.Sleep(2 * time.Second)
return nil
})
}

67
nft/log/default.go Normal file
View File

@ -0,0 +1,67 @@
package log
import (
"fmt"
"os"
"sync"
)
var (
nilLogger = func(prefix, timestamp, msg string, data ...any) {}
normalLogger = func(prefix, timestamp, msg string, data ...any) {
fmt.Printf(prefix+"| "+timestamp+" | "+msg+"\n", data...)
}
panicLogger = func(prefix, timestamp, msg string, data ...any) {
panic(fmt.Sprintf(prefix+"| "+timestamp+" | "+msg+"\n", data...))
}
fatalLogger = func(prefix, timestamp, msg string, data ...any) {
fmt.Printf(prefix+"| "+timestamp+" | "+msg+"\n", data...)
os.Exit(1)
}
DefaultLogger = &logger{
Mutex: sync.Mutex{},
timeFormat: "2006-01-02T15:04:05",
writer: os.Stdout,
level: LogLevelInfo,
debug: nilLogger,
info: normalLogger,
warn: normalLogger,
error: normalLogger,
panic: panicLogger,
fatal: fatalLogger,
}
)
func SetTimeFormat(format string) {
DefaultLogger.SetTimeFormat(format)
}
func SetLogLevel(level LogLevel) {
DefaultLogger.SetLogLevel(level)
}
func Debug(msg string, data ...any) {
DefaultLogger.Debug(msg, data...)
}
func Info(msg string, data ...any) {
DefaultLogger.Info(msg, data...)
}
func Warn(msg string, data ...any) {
DefaultLogger.Warn(msg, data...)
}
func Error(msg string, data ...any) {
DefaultLogger.Error(msg, data...)
}
func Panic(msg string, data ...any) {
DefaultLogger.Panic(msg, data...)
}
func Fatal(msg string, data ...any) {
DefaultLogger.Fatal(msg, data...)
}

115
nft/log/log.go Normal file
View File

@ -0,0 +1,115 @@
package log
import (
"github.com/fatih/color"
"io"
"sync"
"time"
)
type LogLevel uint32
const (
LogLevelDebug = iota
LogLevelInfo
LogLevelWarn
LogLevelError
LogLevelPanic
LogLevelFatal
)
type logger struct {
sync.Mutex
timeFormat string
writer io.Writer
level LogLevel
debug func(prefix, timestamp, msg string, data ...any)
info func(prefix, timestamp, msg string, data ...any)
warn func(prefix, timestamp, msg string, data ...any)
error func(prefix, timestamp, msg string, data ...any)
panic func(prefix, timestamp, msg string, data ...any)
fatal func(prefix, timestamp, msg string, data ...any)
}
var (
red = color.New(color.FgRed)
hired = color.New(color.FgHiRed)
green = color.New(color.FgGreen)
yellow = color.New(color.FgYellow)
white = color.New(color.FgWhite)
)
func (l *logger) SetTimeFormat(format string) {
l.Lock()
defer l.Unlock()
l.timeFormat = format
}
func (l *logger) SetLogLevel(level LogLevel) {
l.Lock()
defer l.Unlock()
if level > LogLevelDebug {
l.debug = nilLogger
} else {
l.debug = normalLogger
}
if level > LogLevelInfo {
l.info = nilLogger
} else {
l.info = normalLogger
}
if level > LogLevelWarn {
l.warn = nilLogger
} else {
l.warn = normalLogger
}
if level > LogLevelError {
l.error = nilLogger
} else {
l.error = normalLogger
}
if level > LogLevelPanic {
l.panic = nilLogger
} else {
l.panic = panicLogger
}
if level > LogLevelFatal {
l.fatal = nilLogger
} else {
l.fatal = fatalLogger
}
}
func (l *logger) Debug(msg string, data ...any) {
l.debug(white.Sprint("Debug "), time.Now().Format(l.timeFormat), msg, data...)
}
func (l *logger) Info(msg string, data ...any) {
l.info(green.Sprint("Info "), time.Now().Format(l.timeFormat), msg, data...)
}
func (l *logger) Warn(msg string, data ...any) {
l.warn(yellow.Sprint("Warn "), time.Now().Format(l.timeFormat), msg, data...)
}
func (l *logger) Error(msg string, data ...any) {
l.error(red.Sprint("Error "), time.Now().Format(l.timeFormat), msg, data...)
}
func (l *logger) Panic(msg string, data ...any) {
l.panic(hired.Sprint("Panic "), time.Now().Format(l.timeFormat), msg, data...)
}
func (l *logger) Fatal(msg string, data ...any) {
l.fatal(hired.Sprint("Fatal "), time.Now().Format(l.timeFormat), msg, data...)
}
type WroteLogger interface {
Info(msg string, data ...any)
}

21
nft/log/new.go Normal file
View File

@ -0,0 +1,21 @@
package log
import (
"os"
"sync"
)
func New() *logger {
return &logger{
Mutex: sync.Mutex{},
timeFormat: "2006-01-02T15:04:05",
writer: os.Stdout,
level: LogLevelInfo,
debug: nilLogger,
info: normalLogger,
warn: normalLogger,
error: normalLogger,
panic: panicLogger,
fatal: fatalLogger,
}
}

View File

@ -0,0 +1,149 @@
package cmd
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"text/template"
"github.com/loveuer/nf/nft/loading"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/nfctl/internal/opt"
"github.com/loveuer/nf/nft/tool"
"github.com/spf13/cobra"
)
var newCmd = &cobra.Command{
Use: "new",
Short: "new a nf project",
Example: "nfctl new <project> -t ultone [options]",
RunE: doNew,
SilenceErrors: true,
}
func initNew() *cobra.Command {
newCmd.Flags().StringVarP(&opt.Cfg.New.Template, "template", "t", "ultone", "template name/url[example:ultone, https://gitea.loveuer.com/loveuer/ultone.git]")
newCmd.Flags().BoolVar(&opt.Cfg.New.DisableInitScript, "disable-init-script", false, "disable init script(.nfctl)")
return newCmd
}
func doNew(cmd *cobra.Command, args []string) (err error) {
if len(args) == 0 {
return errors.New("必须提供 project 名称")
}
if strings.HasSuffix(args[0], "/") {
return errors.New("project 名称不能以 / 结尾")
}
base := path.Base(args[0])
if strings.HasPrefix(base, ".") {
return errors.New("project 名称不能以 . 开头")
}
return loading.Do(cmd.Context(), func(ctx context.Context, print func(msg string, types ...loading.Type)) error {
print("开始新建项目: "+args[0], loading.TypeInfo)
pwd, err := os.Getwd()
if err != nil {
return err
}
moduleName := args[0]
pwd = path.Join(filepath.ToSlash(pwd), base)
log.Debug("cmd.new: new project, pwd = %s, name = %s, template = %s", pwd, moduleName, opt.Cfg.New.Template)
print("开始下载模板: "+opt.Cfg.New.Template, loading.TypeProcessing)
repo := opt.Cfg.New.Template
if v, ok := opt.TemplateMap[repo]; ok {
repo = v
}
if err = tool.Clone(pwd, repo); err != nil {
return err
}
print("下载模板完成: "+opt.Cfg.New.Template, loading.TypeSuccess)
if err = os.RemoveAll(path.Join(pwd, ".git")); err != nil {
print(err.Error(), loading.TypeWarning)
}
print("开始初始化项目: "+args[0], loading.TypeProcessing)
if err = filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "go.mod") {
var content []byte
if content, err = os.ReadFile(path); err != nil {
print("初始化文件失败: "+err.Error(), loading.TypeWarning)
print("开始初始化项目: "+args[0], loading.TypeProcessing)
return nil
}
scanner := bufio.NewScanner(bytes.NewReader(content))
replaced := make([]string, 0, 16)
for scanner.Scan() {
line := scanner.Text()
// 操作 go.mod 文件时, 忽略 toolchain 行, 以更好的兼容 go1.20
if strings.HasSuffix(path, "go.mod") && strings.HasPrefix(line, "toolchain") {
continue
}
replaced = append(replaced, strings.ReplaceAll(line, opt.Cfg.New.Template, moduleName))
}
if err = os.WriteFile(path, []byte(strings.Join(replaced, "\n")), 0o644); err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
var (
render *template.Template
rf *os.File
)
if render, err = template.New(base).Parse(opt.README); err != nil {
log.Debug("cmd.new: new text template err, err = %s", err.Error())
print("生成 readme 失败", loading.TypeWarning)
goto END
}
if rf, err = os.OpenFile(path.Join(pwd, "readme.md"), os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o644); err != nil {
log.Debug("cmd.new: new readme file err, err = %s", err.Error())
print("生成 readme 失败", loading.TypeWarning)
goto END
}
defer rf.Close()
if err = render.Execute(rf, map[string]any{
"project_name": base,
}); err != nil {
log.Debug("cmd.new: template execute err, err = %s", err.Error())
print("生成 readme 失败", loading.TypeWarning)
}
END:
print(fmt.Sprintf("项目: %s 初始化成功", args[0]), loading.TypeSuccess)
return nil
})
}

View File

@ -0,0 +1,41 @@
package cmd
import (
"os"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/nfctl/internal/opt"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "nfctl",
Short: "nfctl is a tool for quick start a nf projects",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if opt.Cfg.Debug {
log.SetLogLevel(log.LogLevelDebug)
}
if opt.Cfg.Version {
doVersion(cmd, args)
os.Exit(0)
}
if !opt.Cfg.DisableUpdate {
doUpdate(cmd.Context())
}
return nil
},
DisableSuggestions: true,
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {},
}
func initRoot(cmds ...*cobra.Command) {
rootCmd.PersistentFlags().BoolVar(&opt.Cfg.Debug, "debug", false, "debug mode")
rootCmd.PersistentFlags().BoolVar(&opt.Cfg.DisableUpdate, "disable-update", false, "disable self update")
rootCmd.PersistentFlags().BoolVarP(&opt.Cfg.Version, "version", "v", false, "print nfctl version")
rootCmd.AddCommand(cmds...)
}

View File

@ -0,0 +1,83 @@
package cmd
import (
"context"
"crypto/tls"
"fmt"
"regexp"
"strings"
"time"
resty "github.com/go-resty/resty/v2"
"github.com/loveuer/nf/nft/loading"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/nfctl/internal/opt"
"github.com/loveuer/nf/nft/tool"
"github.com/spf13/cobra"
)
var updateCmd = &cobra.Command{
Use: "update",
Short: "update nfctl self",
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
func initUpdate() *cobra.Command {
return updateCmd
}
func doUpdate(ctx context.Context) (err error) {
return loading.Do(tool.TimeoutCtx(ctx, 30), func(ctx context.Context, print func(msg string, types ...loading.Type)) error {
print("正在检查更新...")
tip := "❗ 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@master"
version := ""
var rr *resty.Response
if rr, err = resty.New().SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).R().
SetContext(ctx).
Get(opt.VersionURL); err != nil {
err = fmt.Errorf("检查更新失败: %s\n%s", err.Error(), tip)
return err
}
log.Debug("cmd.update: url = %s, raw_response = %s", opt.VersionURL, rr.String())
if rr.StatusCode() != 200 {
err = fmt.Errorf("检查更新失败: %s\n%s", rr.Status(), tip)
return err
}
reg := regexp.MustCompile(`const Version = "v\d{2}\.\d{2}\.\d{2}-r\d{1,2}"`)
for _, line := range strings.Split(rr.String(), "\n") {
if reg.MatchString(line) {
version = strings.TrimSpace(strings.TrimPrefix(line, "const Version = "))
version = version[1 : len(version)-1]
break
}
}
if version == "" {
err = fmt.Errorf("检查更新失败: 未找到版本信息\n%s", tip)
return err
}
log.Debug("cmd.update: find version = %s, now_version = %s", version, opt.Version)
if version <= opt.Version {
print(fmt.Sprintf("已是最新版本: %s", opt.Version), loading.TypeSuccess)
return nil
}
print(fmt.Sprintf("发现新版本: %s", version), loading.TypeInfo)
print(fmt.Sprintf("正在更新到 %s ...", version))
time.Sleep(2 * time.Second)
print("暂时无法自动更新, 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@master", loading.TypeWarning)
return nil
})
}

View File

@ -0,0 +1,19 @@
package cmd
import (
"fmt"
"github.com/loveuer/nf/nft/nfctl/internal/opt"
"github.com/spf13/cobra"
)
func initVersion() *cobra.Command {
return &cobra.Command{
Use: "version",
Run: doVersion,
}
}
func doVersion(cmd *cobra.Command, args []string) {
fmt.Printf("%s\nnfctl: %s\n\n", opt.Banner, opt.Version)
}

View File

@ -0,0 +1,26 @@
package cmd
import (
"context"
"fmt"
"os"
"time"
)
func Init() {
initRoot(
initVersion(),
initUpdate(),
initNew(),
)
}
func Run(ctx context.Context) {
if err := rootCmd.ExecuteContext(ctx); err != nil {
fmt.Printf("❌ %s\n", err.Error())
time.Sleep(300 * time.Millisecond)
os.Exit(1)
}
time.Sleep(300 * time.Millisecond)
}

View File

@ -0,0 +1,21 @@
package opt
type _new struct {
Template string
DisableInitScript bool
}
type config struct {
Debug bool
DisableUpdate bool
Version bool
New _new
}
var Cfg = &config{}
var TemplateMap = map[string]string{
"ultone": "https://gitea.loveuer.com/loveuer/ultone.git",
}
const README = "# {{ .project_name }}\n\n### 启动\n- `go run . --help`\n- `go run .`\n\n### 构建\n- `go build -ldflags '-s -w' -o dist/{{ .project_name}}_app .`\n- `docker build -t <image> -f Dockerfile .`"

View File

@ -0,0 +1,13 @@
package opt
const Version = "v24.12.27-r03"
// const VersionURL = "https://github.com/loveuer/nf/nft/nfctl/internal/opt/version.go"
const VersionURL = "https://raw.githubusercontent.com/loveuer/nf/refs/heads/master/nft/nfctl/internal/opt/version.go"
const Banner = ` ___ __ __
___ / _/___/ /_/ /
/ _ \/ _/ __/ __/ /
/_//_/_/ \__/\__/_/
`

View File

@ -0,0 +1,10 @@
package opt
import (
"fmt"
"testing"
)
func TestBanner(t *testing.T) {
fmt.Printf("%s\nnfctl: %s\n\n", Banner, Version)
}

22
nft/nfctl/main.go Normal file
View File

@ -0,0 +1,22 @@
package main
import (
"context"
"os/signal"
"syscall"
"time"
"github.com/loveuer/nf/nft/nfctl/internal/cmd"
)
func init() {
time.Local = time.FixedZone("CST", 8*3600)
cmd.Init()
}
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
defer cancel()
cmd.Run(ctx)
}

View File

@ -2,9 +2,10 @@ package resp
import (
"fmt"
"github.com/loveuer/nf"
"strconv"
"strings"
"github.com/loveuer/nf"
)
func handleEmptyMsg(status uint32, msg string) string {
@ -102,6 +103,18 @@ func Resp403(c *nf.Ctx, data any, msgs ...string) error {
return Resp(c, 403, msg, err, data)
}
func Resp418(c *nf.Ctx, data any, msgs ...string) error {
msg := MSG418
err := ""
if len(msgs) > 0 && msgs[0] != "" {
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
err = ""
}
return Resp(c, 418, msg, err, data)
}
func Resp429(c *nf.Ctx, data any, msgs ...string) error {
msg := MSG429
err := ""

View File

@ -7,6 +7,7 @@ const (
MSG401 = "登录已过期, 请重新登录"
MSG403 = "请求权限不足"
MSG404 = "请求资源未找到"
MSG418 = "请求条件不满足, 请稍后再试"
MSG429 = "请求过于频繁, 请稍后再试"
MSG500 = "服务器开小差了, 请稍后再试"
MSG501 = "功能开发中, 尽情期待"

40
nft/tool/clone.go Normal file
View File

@ -0,0 +1,40 @@
package tool
import (
"fmt"
"net/url"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/transport/http"
)
func Clone(projectDir string, repoURL string) error {
ins, err := url.Parse(repoURL)
if err != nil {
return err
}
uri := fmt.Sprintf("%s://%s%s", ins.Scheme, ins.Host, ins.Path)
opt := &git.CloneOptions{
URL: uri,
Depth: 1,
InsecureSkipTLS: true,
SingleBranch: true,
}
if ins.User != nil {
password, _ := ins.User.Password()
opt.Auth = &http.BasicAuth{
Username: ins.User.Username(),
Password: password,
}
}
_, err = git.PlainClone(projectDir, false, opt)
if err != nil {
return err
}
return nil
}

34
nft/tool/ctx.go Normal file
View File

@ -0,0 +1,34 @@
package tool
import (
"context"
"time"
)
func Timeout(seconds ...int) (ctx context.Context) {
var duration time.Duration
if len(seconds) > 0 && seconds[0] > 0 {
duration = time.Duration(seconds[0]) * time.Second
} else {
duration = time.Duration(30) * time.Second
}
ctx, _ = context.WithTimeout(context.Background(), duration)
return
}
func TimeoutCtx(ctx context.Context, seconds ...int) context.Context {
var duration time.Duration
if len(seconds) > 0 && seconds[0] > 0 {
duration = time.Duration(seconds[0]) * time.Second
} else {
duration = time.Duration(30) * time.Second
}
nctx, _ := context.WithTimeout(ctx, duration)
return nctx
}

135
readme.md
View File

@ -5,63 +5,98 @@
##### basic usage
- get param
```go
func main() {
app := nf.New()
app.Get("/hello/:name", func(c *nf.Ctx) error {
name := c.Param("name")
return c.JSON(nf.Map{"status": 200, "data": "hello, " + name})
})
```go
func main() {
app := nf.New()
log.Fatal(app.Run("0.0.0.0:80"))
}
```
app.Get("/hello/:name", func(c *nf.Ctx) error {
name := c.Param("name")
return c.JSON(nf.Map{"status": 200, "data": "hello, " + name})
})
log.Fatal(app.Run("0.0.0.0:80"))
}
```
- parse request query
```go
func handleQuery(c *nf.Ctx) error {
type Req struct {
Name string `query:"name"`
Addr []string `query:"addr"`
}
var (
err error
req = Req{}
)
```go
func handleQuery(c *nf.Ctx) error {
type Req struct {
Name string `query:"name"`
Addr []string `query:"addr"`
}
if err = c.QueryParser(&req); err != nil {
return nf.NewNFError(400, err.Error())
}
var (
err error
req = Req{}
)
return c.JSON(nf.Map{"query": req})
}
```
if err = c.QueryParser(&req); err != nil {
return nf.NewNFError(400, err.Error())
}
return c.JSON(nf.Map{"query": req})
}
```
- parse application/json body
```go
func handlePost(c *nf.Ctx) error {
type Req struct {
Name string `json:"name"`
Addr []string `json:"addr"`
}
var (
err error
req = Req{}
reqMap = make(map[string]interface{})
)
if err = c.BodyParser(&req); err != nil {
return nf.NewNFError(400, err.Error())
}
// can parse body multi times
if err = c.BodyParser(&reqMap); err != nil {
return nf.NewNFError(400, err.Error())
}
return c.JSON(nf.Map{"struct": req, "map": reqMap})
}
```
```go
func handlePost(c *nf.Ctx) error {
type Req struct {
Name string `json:"name"`
Addr []string `json:"addr"`
}
var (
err error
req = Req{}
reqMap = make(map[string]interface{})
)
if err = c.BodyParser(&req); err != nil {
return nf.NewNFError(400, err.Error())
}
// can parse body multi times
if err = c.BodyParser(&reqMap); err != nil {
return nf.NewNFError(400, err.Error())
}
return c.JSON(nf.Map{"struct": req, "map": reqMap})
}
```
- pass local value
```go
type User struct {
Id int
Username string
}
func main() {
app := nf.New()
app.Use(auth())
app.Get("/item/list", list)
}
func auth() nf.HandlerFunc {
return func(c *nf.Ctx) error {
c.Locals("user", &User{Id: 1, Username:"user"})
return c.Next()
}
}
func list(c *nf.Ctx) error {
user, ok := c.Locals("user").(*User)
if !ok {
return c.Status(401).SendString("login required")
}
...
}
```

View File

@ -1 +0,0 @@
package nf

View File

@ -3,6 +3,7 @@ package nf
import (
"bufio"
"io"
"log"
"net"
"net/http"
)
@ -41,8 +42,9 @@ type ResponseWriter interface {
type responseWriter struct {
http.ResponseWriter
size int
status int
written bool
size int
status int
}
var _ ResponseWriter = (*responseWriter)(nil)
@ -60,7 +62,7 @@ func (w *responseWriter) reset(writer http.ResponseWriter) {
func (w *responseWriter) WriteHeader(code int) {
if code > 0 && w.status != code {
if w.Written() {
// todo: debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)
log.Printf("[NF] WARNING: Headers were already written. Wanted to override status code %d with %d", w.status, code)
return
}
w.status = code
@ -102,7 +104,7 @@ func (w *responseWriter) Size() int {
}
func (w *responseWriter) Written() bool {
return w.size != noWritten
return w.size != noWritten || w.written
}
// Hijack implements the http.Hijacker interface.

21
util.go
View File

@ -202,3 +202,24 @@ func bufApp(buf *[]byte, s string, w int, c byte) {
}
b[w] = c
}
func HumanDuration(nano int64) string {
duration := float64(nano)
unit := "ns"
if duration >= 1000 {
duration /= 1000
unit = "us"
}
if duration >= 1000 {
duration /= 1000
unit = "ms"
}
if duration >= 1000 {
duration /= 1000
unit = " s"
}
return fmt.Sprintf("%6.2f%s", duration, unit)
}

View File

@ -1,14 +0,0 @@
### basic - get
GET http://127.0.0.1/hello/nf
### test resp error
GET http://127.0.0.1/error
### test basic post
POST http://127.0.0.1/data
Content-Type: application/json
{
"name": "nice"
}

View File

@ -1,17 +0,0 @@
package main
import (
"github.com/loveuer/nf"
"log"
)
func main() {
app := nf.New(nf.Config{EnableNotImplementHandler: true})
api := app.Group("/api")
api.Get("/1", func(c *nf.Ctx) error {
return c.SendString("nice")
})
log.Fatal(app.Run(":80"))
}

View File

@ -1,9 +0,0 @@
### body_limit
POST http://127.0.0.1/data
Content-Type: application/json
{
"name": "zyp",
"age": 19,
"likes": ["2233"]
}

View File

@ -1,50 +0,0 @@
package main
import (
"github.com/loveuer/nf"
"log"
)
func main() {
app := nf.New(nf.Config{BodyLimit: 30})
app.Post("/data", func(c *nf.Ctx) error {
type Req struct {
Name string `json:"name"`
Age int `json:"age"`
Likes []string `json:"likes"`
}
var (
err error
req = new(Req)
)
if err = c.BodyParser(req); err != nil {
return c.JSON(nf.Map{"status": 400, "err": err.Error()})
}
return c.JSON(nf.Map{"status": 200, "data": req})
})
app.Post("/url", func(c *nf.Ctx) error {
type Req struct {
Name string `form:"name"`
Age int `form:"age"`
Likes []string `form:"likes"`
}
var (
err error
req = new(Req)
)
if err = c.BodyParser(req); err != nil {
return c.JSON(nf.Map{"status": 400, "err": err.Error()})
}
return c.JSON(nf.Map{"status": 200, "data": req})
})
log.Fatal(app.Run("0.0.0.0:80"))
}

View File

@ -1,28 +0,0 @@
package main
import (
"github.com/loveuer/nf"
"log"
)
func main() {
app := nf.New(nf.Config{DisableLogger: false})
app.Get("/hello", func(c *nf.Ctx) error {
return c.SendString("world")
})
app.Use(ml())
log.Fatal(app.Run(":80"))
}
func ml() nf.HandlerFunc {
return func(c *nf.Ctx) error {
index := []byte(`<h1>my not found</h1>`)
c.Set("Content-Type", "text/html")
c.Status(403)
_, err := c.Write(index)
return err
}
}

View File

@ -1,34 +0,0 @@
package main
import (
"errors"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/resp"
"log"
)
func main() {
app := nf.New()
api := app.Group("/api")
api.Get("/hello",
auth(),
func(c *nf.Ctx) error {
return resp.Resp403(c, errors.New("in hello"))
},
)
log.Fatal(app.Run(":80"))
}
func auth() nf.HandlerFunc {
return func(c *nf.Ctx) error {
token := c.Query("token")
if token != "zyp" {
return resp.Resp401(c, errors.New("no auth"))
}
return c.Next()
}
}

View File

@ -1,28 +0,0 @@
package main
import (
"github.com/loveuer/nf"
"log"
)
func main() {
app := nf.New()
app.Get("/nice", h1, h2)
log.Fatal(app.Run(":80"))
}
func h1(c *nf.Ctx) error {
you := c.Query("to")
if you == "you" {
return c.JSON(nf.Map{"status": 201, "msg": "nice to meet you"})
}
//return c.Next()
return nil
}
func h2(c *nf.Ctx) error {
return c.JSON(nf.Map{"status": 200, "msg": "hello world"})
}

View File

@ -1,5 +0,0 @@
### test multi handlers no next
GET http://127.0.0.1:3333/nice?to=you
### test multi handlers do next
GET http://127.0.0.1:3333/nice?to=nf

View File

@ -1,24 +0,0 @@
package main
import (
"github.com/loveuer/nf"
"log"
)
func main() {
app := nf.New(nf.Config{
DisableRecover: false,
})
app.Get("/hello/:name", func(c *nf.Ctx) error {
name := c.Param("name")
if name == "nf" {
panic("name is nf")
}
return c.JSON("nice")
})
log.Fatal(app.Run("0.0.0.0:80"))
}

View File

@ -1,5 +0,0 @@
### panic test
GET http://127.0.0.1/hello/nf
### if covered?
GET http://127.0.0.1/hello/world

View File

@ -1,36 +0,0 @@
package main
import (
"github.com/loveuer/nf"
"log"
)
func main() {
app := nf.New()
app.Get("/hello", func(c *nf.Ctx) error {
type Req struct {
Name string `query:"name"`
Age int `query:"age"`
Likes []string `query:"likes"`
}
var (
err error
req = new(Req)
rm = make(map[string]interface{})
)
//if err = c.QueryParser(req); err != nil {
// return nf.NewNFError(400, "1:"+err.Error())
//}
if err = c.QueryParser(&rm); err != nil {
return nf.NewNFError(400, "2:"+err.Error())
}
return c.JSON(nf.Map{"status": 200, "data": req, "map": rm})
})
log.Fatal(app.Run("0.0.0.0:80"))
}

View File

@ -1,52 +0,0 @@
package main
import (
"context"
"github.com/loveuer/nf"
"log"
"time"
)
var (
app = nf.New()
quit = make(chan bool)
)
func main() {
app.Get("/name", handleGet)
go func() {
err := app.Run(":80")
log.Print("run with err=", err)
quit <- true
}()
<-quit
}
func handleGet(c *nf.Ctx) error {
type Req struct {
Name string `query:"name"`
Addr []string `query:"addr"`
}
var (
err error
req = Req{}
)
if err = c.QueryParser(&req); err != nil {
return nf.NewNFError(400, err.Error())
}
if req.Name == "quit" {
go func() {
time.Sleep(2 * time.Second)
log.Print("app quit = ", app.Shutdown(context.TODO()))
}()
}
return c.JSON(nf.Map{"req_map": req})
}

View File

@ -1,119 +0,0 @@
package main
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"github.com/loveuer/nf"
"log"
"math/big"
"net"
"time"
)
func main() {
app := nf.New(nf.Config{
DisableHttpErrorLog: true,
})
app.Get("/hello/:name", func(c *nf.Ctx) error {
return c.SendString("hello, " + c.Param("name"))
})
st, _, _ := GenerateTlsConfig()
log.Fatal(app.RunTLS(":443", st))
}
func GenerateTlsConfig() (serverTLSConf *tls.Config, clientTLSConf *tls.Config, err error) {
ca := &x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
Organization: []string{"Company, INC."},
Country: []string{"US"},
Province: []string{""},
Locality: []string{"San Francisco"},
StreetAddress: []string{"Golden Gate Bridge"},
PostalCode: []string{"94016"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(99, 0, 0),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
// create our private and public key
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, nil, err
}
// create the CA
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
if err != nil {
return nil, nil, err
}
// pem encode
caPEM := new(bytes.Buffer)
pem.Encode(caPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: caBytes,
})
caPrivKeyPEM := new(bytes.Buffer)
pem.Encode(caPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey),
})
// set up our server certificate
cert := &x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
Organization: []string{"Company, INC."},
Country: []string{"US"},
Province: []string{""},
Locality: []string{"San Francisco"},
StreetAddress: []string{"Golden Gate Bridge"},
PostalCode: []string{"94016"},
},
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
SubjectKeyId: []byte{1, 2, 3, 4, 6},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature,
}
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, nil, err
}
certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)
if err != nil {
return nil, nil, err
}
certPEM := new(bytes.Buffer)
pem.Encode(certPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
})
certPrivKeyPEM := new(bytes.Buffer)
pem.Encode(certPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
})
serverCert, err := tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes())
if err != nil {
return nil, nil, err
}
serverTLSConf = &tls.Config{
Certificates: []tls.Certificate{serverCert},
}
certpool := x509.NewCertPool()
certpool.AppendCertsFromPEM(caPEM.Bytes())
clientTLSConf = &tls.Config{
RootCAs: certpool,
}
return
}