Compare commits
30 Commits
7cf7ec32ac
...
c108679fc9
Author | SHA1 | Date | |
---|---|---|---|
|
c108679fc9 | ||
|
8235631d4f | ||
|
ad6b4fe7b6 | ||
|
d8d771aec6 | ||
|
1e66a221e0 | ||
|
df318682fa | ||
|
af1e58bce9 | ||
|
940e86bd8d | ||
|
5263cba44a | ||
|
63f7516667 | ||
|
9b7f1e4413 | ||
|
e4a6228b0a | ||
|
fb97d6e811 | ||
|
f2a73056c8 | ||
|
52ec9a0686 | ||
|
cbb959a31e | ||
|
b267cc7a2e | ||
|
0f139cda98 | ||
|
8a423c2887 | ||
|
bf1c5ad92f | ||
|
9b7c8d9d24 | ||
|
c13263fe0d | ||
|
d4fe4e0112 | ||
|
16541e377c | ||
|
479c4eef57 | ||
|
436264117c | ||
|
56fa3815cb | ||
|
9530fa863f | ||
|
f3fb259eee | ||
|
67c15513a2 |
62
.github/workflows/nfctl.yml
vendored
Normal file
62
.github/workflows/nfctl.yml
vendored
Normal 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
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
xtest
|
57
app.go
57
app.go
@ -5,13 +5,15 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/loveuer/nf/internal/bytesconv"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/loveuer/nf/internal/bytesconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -19,8 +21,6 @@ var (
|
|||||||
|
|
||||||
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
|
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
|
||||||
regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
|
regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
|
||||||
|
|
||||||
mimePlain = []string{"text/plain"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
@ -31,6 +31,8 @@ type App struct {
|
|||||||
|
|
||||||
trees methodTrees
|
trees methodTrees
|
||||||
|
|
||||||
|
pool *sync.Pool
|
||||||
|
|
||||||
maxParams uint16
|
maxParams uint16
|
||||||
maxSections uint16
|
maxSections uint16
|
||||||
|
|
||||||
@ -42,13 +44,34 @@ type App struct {
|
|||||||
removeExtraSlash bool // false
|
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) {
|
func (a *App) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
c = newContext(a, writer, request)
|
c = a.pool.Get().(*Ctx)
|
||||||
nfe = new(Err)
|
nfe = new(Err)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
c.reset(writer, request)
|
||||||
|
|
||||||
if err = c.verify(); err != nil {
|
if err = c.verify(); err != nil {
|
||||||
if errors.As(err, nfe) {
|
if errors.As(err, nfe) {
|
||||||
_ = c.Status(nfe.Status).SendString(nfe.Msg)
|
_ = 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.handleHTTPRequest(c)
|
||||||
|
|
||||||
|
a.pool.Put(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) run(ln net.Listener) error {
|
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) {
|
func (a *App) handleHTTPRequest(c *Ctx) {
|
||||||
var (
|
var err error
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
httpMethod := c.Request.Method
|
httpMethod := c.Request.Method
|
||||||
rPath := c.Request.URL.Path
|
rPath := c.Request.URL.Path
|
||||||
@ -165,8 +188,9 @@ func (a *App) handleHTTPRequest(c *Ctx) {
|
|||||||
// Find route in tree
|
// Find route in tree
|
||||||
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
|
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
|
||||||
if value.params != nil {
|
if value.params != nil {
|
||||||
c.Params = *value.params
|
c.params = value.params
|
||||||
}
|
}
|
||||||
|
|
||||||
if value.handlers != nil {
|
if value.handlers != nil {
|
||||||
c.handlers = value.handlers
|
c.handlers = value.handlers
|
||||||
c.fullPath = value.fullPath
|
c.fullPath = value.fullPath
|
||||||
@ -175,7 +199,6 @@ func (a *App) handleHTTPRequest(c *Ctx) {
|
|||||||
serveError(c, errorHandler)
|
serveError(c, errorHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.writermem.WriteHeaderNow()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if httpMethod != http.MethodConnect && rPath != "/" {
|
if httpMethod != http.MethodConnect && rPath != "/" {
|
||||||
@ -204,17 +227,19 @@ func (a *App) handleHTTPRequest(c *Ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(allowed) > 0 {
|
if len(allowed) > 0 {
|
||||||
c.handlers = a.combineHandlers()
|
c.handlers = a.combineHandlers(a.config.MethodNotAllowedHandler)
|
||||||
|
|
||||||
serveError(c, a.config.MethodNotAllowedHandler)
|
_ = c.Next()
|
||||||
|
|
||||||
return
|
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 {
|
func errorHandler(c *Ctx) error {
|
||||||
@ -263,7 +288,7 @@ func redirectFixedPath(c *Ctx, root *node, trailingSlash bool) bool {
|
|||||||
|
|
||||||
func redirectRequest(c *Ctx) {
|
func redirectRequest(c *Ctx) {
|
||||||
req := c.Request
|
req := c.Request
|
||||||
//rPath := req.URL.Path
|
// rPath := req.URL.Path
|
||||||
rURL := req.URL.String()
|
rURL := req.URL.String()
|
||||||
|
|
||||||
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
|
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
|
||||||
@ -271,8 +296,8 @@ func redirectRequest(c *Ctx) {
|
|||||||
code = http.StatusTemporaryRedirect
|
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()
|
c.writermem.WriteHeaderNow()
|
||||||
}
|
}
|
||||||
|
191
ctx.go
191
ctx.go
@ -2,24 +2,31 @@ package nf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"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 {
|
type Ctx struct {
|
||||||
writermem responseWriter
|
lock sync.Mutex
|
||||||
// origin objects
|
writermem responseWriter
|
||||||
writer http.ResponseWriter
|
Writer ResponseWriter
|
||||||
Request *http.Request
|
Request *http.Request
|
||||||
// request info
|
path string
|
||||||
path string
|
method string
|
||||||
method string
|
|
||||||
// response info
|
|
||||||
StatusCode int
|
StatusCode int
|
||||||
|
|
||||||
app *App
|
app *App
|
||||||
@ -29,37 +36,31 @@ type Ctx struct {
|
|||||||
locals map[string]interface{}
|
locals map[string]interface{}
|
||||||
skippedNodes *[]skippedNode
|
skippedNodes *[]skippedNode
|
||||||
fullPath string
|
fullPath string
|
||||||
Params Params
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newContext(app *App, writer http.ResponseWriter, request *http.Request) *Ctx {
|
func (c *Ctx) reset(w http.ResponseWriter, r *http.Request) {
|
||||||
|
traceId := r.Header.Get(TraceKey)
|
||||||
skippedNodes := make([]skippedNode, 0, app.maxSections)
|
if traceId == "" {
|
||||||
v := make(Params, 0, app.maxParams)
|
traceId = uuid.Must(uuid.NewV7()).String()
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.writermem = responseWriter{
|
c.writermem.reset(w)
|
||||||
ResponseWriter: ctx.writer,
|
|
||||||
size: -1,
|
|
||||||
status: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
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{} {
|
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 {
|
func (c *Ctx) Cookies(key string, defaultValue ...string) string {
|
||||||
var (
|
dv := ""
|
||||||
dv = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(defaultValue) > 0 {
|
if len(defaultValue) > 0 {
|
||||||
dv = defaultValue[0]
|
dv = defaultValue[0]
|
||||||
@ -107,15 +106,22 @@ func (c *Ctx) Cookies(key string, defaultValue ...string) string {
|
|||||||
return cookie.Value
|
return cookie.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Context() context.Context {
|
||||||
|
return c.Request.Context()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Ctx) Next() error {
|
func (c *Ctx) Next() error {
|
||||||
c.index++
|
c.index++
|
||||||
|
|
||||||
|
if c.index >= len(c.handlers) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
handler = c.handlers[c.index]
|
handler = c.handlers[c.index]
|
||||||
)
|
)
|
||||||
|
|
||||||
//for c.index < len(c.handlers) {
|
|
||||||
if handler != nil {
|
if handler != nil {
|
||||||
if err = handler(c); err != nil {
|
if err = handler(c); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -123,7 +129,6 @@ func (c *Ctx) Next() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.index++
|
c.index++
|
||||||
//}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -142,18 +147,39 @@ func (c *Ctx) verify() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ctx) Param(key string) string {
|
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 = ¶ms
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ctx) Form(key string) string {
|
func (c *Ctx) Form(key string) string {
|
||||||
return c.Request.FormValue(key)
|
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) {
|
func (c *Ctx) FormFile(key string) (*multipart.FileHeader, error) {
|
||||||
_, fh, err := c.Request.FormFile(key)
|
_, fh, err := c.Request.FormFile(key)
|
||||||
return fh, err
|
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 {
|
func (c *Ctx) Query(key string) string {
|
||||||
return c.Request.URL.Query().Get(key)
|
return c.Request.URL.Query().Get(key)
|
||||||
}
|
}
|
||||||
@ -167,11 +193,24 @@ func (c *Ctx) Get(key string, defaultValue ...string) string {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ctx) IP() string {
|
func (c *Ctx) IP(useProxyHeader ...bool) string {
|
||||||
ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
|
ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
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
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,11 +260,6 @@ func (c *Ctx) BodyParser(out interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ctx) QueryParser(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())
|
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 {
|
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
|
c.StatusCode = c.writermem.status
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set set response header
|
||||||
func (c *Ctx) Set(key string, value string) {
|
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) {
|
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 {
|
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) {
|
func (c *Ctx) Writef(format string, values ...interface{}) (int, error) {
|
||||||
c.SetHeader("Content-Type", "text/plain")
|
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 {
|
func (c *Ctx) JSON(data interface{}) error {
|
||||||
c.SetHeader("Content-Type", MIMEApplicationJSON)
|
c.SetHeader("Content-Type", MIMEApplicationJSON)
|
||||||
|
|
||||||
encoder := json.NewEncoder(&c.writermem)
|
encoder := json.NewEncoder(c.Writer)
|
||||||
|
|
||||||
if err := encoder.Encode(data); err != nil {
|
if err := encoder.Encode(data); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -270,16 +321,44 @@ func (c *Ctx) JSON(data interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ctx) RawWriter() http.ResponseWriter {
|
func (c *Ctx) SSEvent(event string, data interface{}) error {
|
||||||
return c.writer
|
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) {
|
func (c *Ctx) Flush() error {
|
||||||
return c.writermem.Write(data)
|
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 {
|
func (c *Ctx) HTML(html string) error {
|
||||||
c.SetHeader("Content-Type", "text/html")
|
c.SetHeader("Content-Type", "text/html")
|
||||||
_, err := c.writer.Write([]byte(html))
|
_, err := c.Write([]byte(html))
|
||||||
return err
|
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
36
go.mod
@ -1,3 +1,39 @@
|
|||||||
module github.com/loveuer/nf
|
module github.com/loveuer/nf
|
||||||
|
|
||||||
go 1.20
|
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
151
go.sum
@ -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
106
internal/sse/sse-encoder.go
Normal 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
24
internal/sse/writer.go
Normal 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}
|
||||||
|
}
|
||||||
|
}
|
@ -2,10 +2,11 @@ package nf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/loveuer/nf/nft/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRecover(enableStackTrace bool) HandlerFunc {
|
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))
|
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))
|
_ = c.Status(500).SendString(fmt.Sprint(r))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -28,50 +29,28 @@ func NewRecover(enableStackTrace bool) HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewLogger() 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 {
|
return func(c *Ctx) error {
|
||||||
start := time.Now()
|
var (
|
||||||
|
now = time.Now()
|
||||||
|
logFn func(msg string, data ...any)
|
||||||
|
ip = c.IP()
|
||||||
|
)
|
||||||
|
|
||||||
err := c.Next()
|
err := c.Next()
|
||||||
|
duration := time.Since(now)
|
||||||
|
|
||||||
var (
|
msg := fmt.Sprintf("NF | %v | %15s | %3d | %s | %6s | %s", c.Context().Value(TraceKey), ip, c.StatusCode, HumanDuration(duration.Nanoseconds()), c.Method(), c.Path())
|
||||||
duration = time.Now().Sub(start).Nanoseconds()
|
|
||||||
status = c.StatusCode
|
|
||||||
path = c.path
|
|
||||||
method = c.Request.Method
|
|
||||||
)
|
|
||||||
|
|
||||||
l.Printf("%s | %5s | %d | %s | %s",
|
switch {
|
||||||
start.Format("06/01/02T15:04:05"),
|
case c.StatusCode >= 500:
|
||||||
method,
|
logFn = log.Error
|
||||||
status,
|
case c.StatusCode >= 400:
|
||||||
durationFormat(duration),
|
logFn = log.Warn
|
||||||
path,
|
default:
|
||||||
)
|
logFn = log.Info
|
||||||
|
}
|
||||||
|
|
||||||
|
logFn(msg)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
47
nf.go
47
nf.go
@ -1,10 +1,13 @@
|
|||||||
package nf
|
package nf
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
banner = " _ _ _ ___ _ \n | \\| |___| |_ | __|__ _ _ _ _ __| |\n | .` / _ \\ _| | _/ _ \\ || | ' \\/ _` |\n |_|\\_\\___/\\__| |_|\\___/\\_,_|_||_\\__,_|\n "
|
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>"
|
_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`
|
_405 = `405 Method Not Allowed`
|
||||||
_500 = `500 Internal Server Error`
|
_500 = `500 Internal Server Error`
|
||||||
|
TraceKey = "X-Trace-Id"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Map map[string]interface{}
|
type Map map[string]interface{}
|
||||||
@ -22,26 +25,24 @@ type Config struct {
|
|||||||
DisableRecover bool `json:"-"`
|
DisableRecover bool `json:"-"`
|
||||||
DisableHttpErrorLog bool `json:"-"`
|
DisableHttpErrorLog bool `json:"-"`
|
||||||
|
|
||||||
//EnableNotImplementHandler bool `json:"-"`
|
// EnableNotImplementHandler bool `json:"-"`
|
||||||
NotFoundHandler HandlerFunc `json:"-"`
|
NotFoundHandler HandlerFunc `json:"-"`
|
||||||
MethodNotAllowedHandler HandlerFunc `json:"-"`
|
MethodNotAllowedHandler HandlerFunc `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var defaultConfig = &Config{
|
||||||
defaultConfig = &Config{
|
BodyLimit: 4 * 1024 * 1024,
|
||||||
BodyLimit: 4 * 1024 * 1024,
|
NotFoundHandler: func(c *Ctx) error {
|
||||||
NotFoundHandler: func(c *Ctx) error {
|
c.Set("Content-Type", MIMETextHTML)
|
||||||
c.Set("Content-Type", MIMETextHTML)
|
_, err := c.Status(404).Write([]byte(_404))
|
||||||
_, err := c.Status(404).Write([]byte(_404))
|
return err
|
||||||
return err
|
},
|
||||||
},
|
MethodNotAllowedHandler: func(c *Ctx) error {
|
||||||
MethodNotAllowedHandler: func(c *Ctx) error {
|
c.Set("Content-Type", MIMETextPlain)
|
||||||
c.Set("Content-Type", MIMETextPlain)
|
_, err := c.Status(405).Write([]byte(_405))
|
||||||
_, err := c.Status(405).Write([]byte(_405))
|
return err
|
||||||
return err
|
},
|
||||||
},
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func New(config ...Config) *App {
|
func New(config ...Config) *App {
|
||||||
app := &App{
|
app := &App{
|
||||||
@ -51,6 +52,8 @@ func New(config ...Config) *App {
|
|||||||
root: true,
|
root: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
pool: &sync.Pool{},
|
||||||
|
|
||||||
redirectTrailingSlash: true, // true
|
redirectTrailingSlash: true, // true
|
||||||
redirectFixedPath: false, // false
|
redirectFixedPath: false, // false
|
||||||
handleMethodNotAllowed: true, // false
|
handleMethodNotAllowed: true, // false
|
||||||
@ -88,5 +91,9 @@ func New(config ...Config) *App {
|
|||||||
app.Use(NewRecover(true))
|
app.Use(NewRecover(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.pool.New = func() any {
|
||||||
|
return app.allocateContext()
|
||||||
|
}
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
123
nft/loading/loading.go
Normal file
123
nft/loading/loading.go
Normal 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
|
||||||
|
}
|
25
nft/loading/loading_test.go
Normal file
25
nft/loading/loading_test.go
Normal 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
67
nft/log/default.go
Normal 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
115
nft/log/log.go
Normal 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
21
nft/log/new.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
149
nft/nfctl/internal/cmd/cmd.new.go
Normal file
149
nft/nfctl/internal/cmd/cmd.new.go
Normal 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
|
||||||
|
})
|
||||||
|
}
|
41
nft/nfctl/internal/cmd/cmd.root.go
Normal file
41
nft/nfctl/internal/cmd/cmd.root.go
Normal 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...)
|
||||||
|
}
|
83
nft/nfctl/internal/cmd/cmd.update.go
Normal file
83
nft/nfctl/internal/cmd/cmd.update.go
Normal 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
|
||||||
|
})
|
||||||
|
}
|
19
nft/nfctl/internal/cmd/cmd.version.go
Normal file
19
nft/nfctl/internal/cmd/cmd.version.go
Normal 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)
|
||||||
|
}
|
26
nft/nfctl/internal/cmd/init.go
Normal file
26
nft/nfctl/internal/cmd/init.go
Normal 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)
|
||||||
|
}
|
21
nft/nfctl/internal/opt/opt.go
Normal file
21
nft/nfctl/internal/opt/opt.go
Normal 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 .`"
|
13
nft/nfctl/internal/opt/version.go
Normal file
13
nft/nfctl/internal/opt/version.go
Normal 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 = ` ___ __ __
|
||||||
|
___ / _/___/ /_/ /
|
||||||
|
/ _ \/ _/ __/ __/ /
|
||||||
|
/_//_/_/ \__/\__/_/
|
||||||
|
`
|
10
nft/nfctl/internal/opt/version_test.go
Normal file
10
nft/nfctl/internal/opt/version_test.go
Normal 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
22
nft/nfctl/main.go
Normal 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)
|
||||||
|
}
|
@ -2,9 +2,10 @@ package resp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/loveuer/nf"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/loveuer/nf"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleEmptyMsg(status uint32, msg string) string {
|
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)
|
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 {
|
func Resp429(c *nf.Ctx, data any, msgs ...string) error {
|
||||||
msg := MSG429
|
msg := MSG429
|
||||||
err := ""
|
err := ""
|
||||||
|
@ -7,6 +7,7 @@ const (
|
|||||||
MSG401 = "登录已过期, 请重新登录"
|
MSG401 = "登录已过期, 请重新登录"
|
||||||
MSG403 = "请求权限不足"
|
MSG403 = "请求权限不足"
|
||||||
MSG404 = "请求资源未找到"
|
MSG404 = "请求资源未找到"
|
||||||
|
MSG418 = "请求条件不满足, 请稍后再试"
|
||||||
MSG429 = "请求过于频繁, 请稍后再试"
|
MSG429 = "请求过于频繁, 请稍后再试"
|
||||||
MSG500 = "服务器开小差了, 请稍后再试"
|
MSG500 = "服务器开小差了, 请稍后再试"
|
||||||
MSG501 = "功能开发中, 尽情期待"
|
MSG501 = "功能开发中, 尽情期待"
|
||||||
|
40
nft/tool/clone.go
Normal file
40
nft/tool/clone.go
Normal 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
34
nft/tool/ctx.go
Normal 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
135
readme.md
@ -5,63 +5,98 @@
|
|||||||
##### basic usage
|
##### basic usage
|
||||||
|
|
||||||
- get param
|
- get param
|
||||||
```go
|
|
||||||
func main() {
|
|
||||||
app := nf.New()
|
|
||||||
|
|
||||||
app.Get("/hello/:name", func(c *nf.Ctx) error {
|
```go
|
||||||
name := c.Param("name")
|
func main() {
|
||||||
return c.JSON(nf.Map{"status": 200, "data": "hello, " + name})
|
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
|
- parse request query
|
||||||
```go
|
|
||||||
func handleQuery(c *nf.Ctx) error {
|
|
||||||
type Req struct {
|
|
||||||
Name string `query:"name"`
|
|
||||||
Addr []string `query:"addr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
```go
|
||||||
err error
|
func handleQuery(c *nf.Ctx) error {
|
||||||
req = Req{}
|
type Req struct {
|
||||||
)
|
Name string `query:"name"`
|
||||||
|
Addr []string `query:"addr"`
|
||||||
|
}
|
||||||
|
|
||||||
if err = c.QueryParser(&req); err != nil {
|
var (
|
||||||
return nf.NewNFError(400, err.Error())
|
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
|
- parse application/json body
|
||||||
```go
|
|
||||||
func handlePost(c *nf.Ctx) error {
|
|
||||||
type Req struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Addr []string `json:"addr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
```go
|
||||||
err error
|
func handlePost(c *nf.Ctx) error {
|
||||||
req = Req{}
|
type Req struct {
|
||||||
reqMap = make(map[string]interface{})
|
Name string `json:"name"`
|
||||||
)
|
Addr []string `json:"addr"`
|
||||||
|
}
|
||||||
if err = c.BodyParser(&req); err != nil {
|
|
||||||
return nf.NewNFError(400, err.Error())
|
var (
|
||||||
}
|
err error
|
||||||
|
req = Req{}
|
||||||
// can parse body multi times
|
reqMap = make(map[string]interface{})
|
||||||
if err = c.BodyParser(&reqMap); err != nil {
|
)
|
||||||
return nf.NewNFError(400, err.Error())
|
|
||||||
}
|
if err = c.BodyParser(&req); err != nil {
|
||||||
|
return nf.NewNFError(400, err.Error())
|
||||||
return c.JSON(nf.Map{"struct": req, "map": reqMap})
|
}
|
||||||
}
|
|
||||||
```
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
@ -3,6 +3,7 @@ package nf
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
@ -41,8 +42,9 @@ type ResponseWriter interface {
|
|||||||
|
|
||||||
type responseWriter struct {
|
type responseWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
size int
|
written bool
|
||||||
status int
|
size int
|
||||||
|
status int
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ ResponseWriter = (*responseWriter)(nil)
|
var _ ResponseWriter = (*responseWriter)(nil)
|
||||||
@ -60,7 +62,7 @@ func (w *responseWriter) reset(writer http.ResponseWriter) {
|
|||||||
func (w *responseWriter) WriteHeader(code int) {
|
func (w *responseWriter) WriteHeader(code int) {
|
||||||
if code > 0 && w.status != code {
|
if code > 0 && w.status != code {
|
||||||
if w.Written() {
|
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
|
return
|
||||||
}
|
}
|
||||||
w.status = code
|
w.status = code
|
||||||
@ -102,7 +104,7 @@ func (w *responseWriter) Size() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *responseWriter) Written() bool {
|
func (w *responseWriter) Written() bool {
|
||||||
return w.size != noWritten
|
return w.size != noWritten || w.written
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hijack implements the http.Hijacker interface.
|
// Hijack implements the http.Hijacker interface.
|
||||||
|
21
util.go
21
util.go
@ -202,3 +202,24 @@ func bufApp(buf *[]byte, s string, w int, c byte) {
|
|||||||
}
|
}
|
||||||
b[w] = c
|
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)
|
||||||
|
}
|
||||||
|
@ -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"
|
|
||||||
}
|
|
@ -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"))
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
### body_limit
|
|
||||||
POST http://127.0.0.1/data
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "zyp",
|
|
||||||
"age": 19,
|
|
||||||
"likes": ["2233"]
|
|
||||||
}
|
|
@ -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"))
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"})
|
|
||||||
}
|
|
@ -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
|
|
@ -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"))
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
### panic test
|
|
||||||
GET http://127.0.0.1/hello/nf
|
|
||||||
|
|
||||||
### if covered?
|
|
||||||
GET http://127.0.0.1/hello/world
|
|
@ -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"))
|
|
||||||
}
|
|
@ -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})
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user