Compare commits

..

20 Commits

Author SHA1 Message Date
1e66a221e0 feat: add RenderHTML, Redirect 2024-10-25 10:23:52 +08:00
df318682fa fix: nfctl new(project name include '/') 2024-09-22 20:50:19 -07:00
af1e58bce9 update: add resp 418 2024-09-19 01:43:05 -07:00
940e86bd8d chore: update go module, readme 2024-08-27 14:39:24 +08:00
5263cba44a chore: expose default logger
chore: add ctx alias method
2024-08-12 16:14:16 +08:00
63f7516667 fix: c.Writer 2024-07-25 17:23:53 +08:00
9b7f1e4413 chore: X-Trace-Id
fix req context trace id; default logger with trace-id

ci: fix nfctl run
2024-07-17 23:00:45 +08:00
e4a6228b0a feat: add trace_id to context 2024-07-17 18:07:10 +08:00
fb97d6e811 style: nfctl new version print 2024-07-14 21:17:40 +08:00
f2a73056c8 fix: github action 2024-07-14 17:49:43 +08:00
52ec9a0686 chore: nfctl version, check cmd 2024-07-14 17:26:48 +08:00
cbb959a31e chore: add github workflow to build nfctl 2024-07-13 22:37:05 +08:00
b267cc7a2e style: cmd.new add success tip 2024-07-12 16:33:35 +08:00
0f139cda98 feat: add nfctl(ctl to start nf project) 2024-07-12 16:00:15 +08:00
8a423c2887 dev: nft/ctl 2024-07-09 18:08:49 +08:00
bf1c5ad92f feat: c.IP add use-proxy-header arg 2024-07-04 17:37:27 +08:00
9b7c8d9d24 fix: writer 2024-06-25 16:36:43 +08:00
c13263fe0d fix: logger missing status while 404 2024-06-17 14:31:49 +08:00
d4fe4e0112 feat: refact default logger 2024-06-07 17:39:06 +08:00
16541e377c fix: set status not work 2024-05-22 14:08:34 +08:00
30 changed files with 1540 additions and 124 deletions

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

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

12
app.go
View File

@ -202,17 +202,19 @@ func (a *App) handleHTTPRequest(c *Ctx) {
}
if len(allowed) > 0 {
c.handlers = a.combineHandlers()
c.handlers = a.combineHandlers(a.config.MethodNotAllowedHandler)
serveError(c, a.config.MethodNotAllowedHandler)
_ = c.Next()
return
}
}
c.handlers = a.combineHandlers()
c.handlers = a.combineHandlers(a.config.NotFoundHandler)
serveError(c, a.config.NotFoundHandler)
_ = c.Next()
return
}
func errorHandler(c *Ctx) error {
@ -271,6 +273,6 @@ func redirectRequest(c *Ctx) {
//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()
}

97
ctx.go
View File

@ -2,10 +2,13 @@ package nf
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/loveuer/nf/internal/sse"
"html/template"
"io"
"mime/multipart"
"net"
@ -14,10 +17,14 @@ import (
"sync"
)
var (
forwardHeaders = []string{"CF-Connecting-IP", "X-Forwarded-For", "X-Real-Ip"}
)
type Ctx struct {
lock sync.Mutex
writermem responseWriter
writer http.ResponseWriter
Writer ResponseWriter
Request *http.Request
path string
method string
@ -34,13 +41,21 @@ type Ctx struct {
func newContext(app *App, writer http.ResponseWriter, request *http.Request) *Ctx {
skippedNodes := make([]skippedNode, 0, app.maxSections)
v := make(Params, 0, app.maxParams)
var (
traceId string
skippedNodes = make([]skippedNode, 0, app.maxSections)
v = make(Params, 0, app.maxParams)
)
if traceId = request.Header.Get(TraceKey); traceId == "" {
traceId = uuid.Must(uuid.NewV7()).String()
}
c := context.WithValue(request.Context(), TraceKey, traceId)
ctx := &Ctx{
lock: sync.Mutex{},
writer: writer,
Request: request,
Request: request.WithContext(c),
path: request.URL.Path,
method: request.Method,
StatusCode: 200,
@ -54,11 +69,14 @@ func newContext(app *App, writer http.ResponseWriter, request *http.Request) *Ct
}
ctx.writermem = responseWriter{
ResponseWriter: ctx.writer,
ResponseWriter: writer,
size: -1,
status: 0,
}
ctx.Writer = &ctx.writermem
ctx.writermem.Header().Set(TraceKey, traceId)
return ctx
}
@ -107,6 +125,10 @@ func (c *Ctx) Cookies(key string, defaultValue ...string) string {
return cookie.Value
}
func (c *Ctx) Context() context.Context {
return c.Request.Context()
}
func (c *Ctx) Next() error {
c.index++
@ -190,11 +212,24 @@ func (c *Ctx) Get(key string, defaultValue ...string) string {
return value
}
func (c *Ctx) IP() string {
func (c *Ctx) IP(useProxyHeader ...bool) string {
ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
if err != nil {
return ""
}
if len(useProxyHeader) > 0 && useProxyHeader[0] {
for _, h := range forwardHeaders {
for _, rip := range strings.Split(c.Request.Header.Get(h), ",") {
realIP := net.ParseIP(strings.Replace(rip, " ", "", -1))
if check := net.ParseIP(realIP.String()); check != nil {
ip = realIP.String()
break
}
}
}
}
return ip
}
@ -255,23 +290,30 @@ func (c *Ctx) Status(code int) *Ctx {
c.lock.Lock()
defer c.lock.Unlock()
c.StatusCode = code
c.writermem.status = code
c.Writer.WriteHeader(code)
c.StatusCode = c.writermem.status
return c
}
// Set set response header
func (c *Ctx) Set(key string, value string) {
c.writermem.Header().Set(key, value)
c.Writer.Header().Set(key, value)
}
// AddHeader add response header
func (c *Ctx) AddHeader(key string, value string) {
c.Writer.Header().Add(key, value)
}
// SetHeader set response header
func (c *Ctx) SetHeader(key string, value string) {
c.writermem.Header().Set(key, value)
c.Writer.Header().Set(key, value)
}
func (c *Ctx) SendStatus(code int) error {
c.Status(code)
c.writermem.WriteHeaderNow()
c.Writer.WriteHeaderNow()
return nil
}
@ -289,7 +331,7 @@ func (c *Ctx) Writef(format string, values ...interface{}) (int, error) {
func (c *Ctx) JSON(data interface{}) error {
c.SetHeader("Content-Type", MIMEApplicationJSON)
encoder := json.NewEncoder(&c.writermem)
encoder := json.NewEncoder(c.Writer)
if err := encoder.Encode(data); err != nil {
return err
@ -303,11 +345,11 @@ func (c *Ctx) SSEvent(event string, data interface{}) error {
c.Set("Cache-Control", "no-cache")
c.Set("Transfer-Encoding", "chunked")
return sse.Encode(c.writer, sse.Event{Event: event, Data: data})
return sse.Encode(c.Writer, sse.Event{Event: event, Data: data})
}
func (c *Ctx) Flush() error {
if f, ok := c.writer.(http.Flusher); ok {
if f, ok := c.Writer.(http.Flusher); ok {
f.Flush()
return nil
}
@ -315,16 +357,27 @@ func (c *Ctx) Flush() error {
return errors.New("http.Flusher is not implemented")
}
func (c *Ctx) RawWriter() http.ResponseWriter {
return c.writer
}
func (c *Ctx) HTML(html string) error {
c.SetHeader("Content-Type", "text/html")
_, err := c.writer.Write([]byte(html))
_, err := c.Write([]byte(html))
return err
}
func (c *Ctx) Write(data []byte) (int, error) {
return c.writermem.Write(data)
func (c *Ctx) RenderHTML(name, html string, obj any) error {
c.SetHeader("Content-Type", "text/html")
t, err := template.New(name).Parse(html)
if err != nil {
return err
}
return t.Execute(c.Writer, obj)
}
func (c *Ctx) Redirect(url string, code int) error {
http.Redirect(c.Writer, c.Request, url, code)
return nil
}
func (c *Ctx) Write(data []byte) (int, error) {
return c.Writer.Write(data)
}

36
go.mod
View File

@ -1,3 +1,39 @@
module github.com/loveuer/nf
go 1.20
require (
github.com/fatih/color v1.17.0
github.com/go-git/go-git/v5 v5.12.0
github.com/google/uuid v1.6.0
github.com/savioxavier/termlink v1.3.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/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.23.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/tools v0.13.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

150
go.sum
View File

@ -0,0 +1,150 @@
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/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/savioxavier/termlink v1.3.0 h1:3Gl4FzQjUyiHzmoEDfmWEhgIwDiJY4poOQHP+k8ReA4=
github.com/savioxavier/termlink v1.3.0/go.mod h1:5T5ePUlWbxCHIwyF8/Ez1qufOoGM89RCg9NvG+3G3gc=
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.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
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.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
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.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.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.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
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.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
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=

View File

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

9
nf.go
View File

@ -1,10 +1,11 @@
package nf
const (
banner = " _ _ _ ___ _ \n | \\| |___| |_ | __|__ _ _ _ _ __| |\n | .` / _ \\ _| | _/ _ \\ || | ' \\/ _` |\n |_|\\_\\___/\\__| |_|\\___/\\_,_|_||_\\__,_|\n "
_404 = "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1\"><meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"><title>Not Found</title><style>body{background:#333;margin:0;color:#ccc;display:flex;align-items:center;max-height:100vh;height:100vh;justify-content:center}textarea{min-height:5rem;min-width:20rem;text-align:center;border:none;background:0 0;color:#ccc;resize:none;user-input:none;user-select:none;cursor:default;-webkit-user-select:none;-webkit-touch-callout:none;-moz-user-select:none;-ms-user-select:none;outline:0}</style></head><body><textarea id=\"banner\" readonly=\"readonly\"></textarea><script type=\"text/javascript\">let htmlCodes = [\n ' _ _ _ ___ _ ',\n '| \\\\| |___| |_ | __|__ _ _ _ _ __| |',\n '| .` / _ \\\\ _| | _/ _ \\\\ || | \\' \\\\/ _` |',\n '|_|\\\\_\\\\___/\\\\__| |_|\\\\___/\\\\_,_|_||_\\\\__,_|'\n].join('\\n');\ndocument.querySelector('#banner').value = htmlCodes</script></body></html>"
_405 = `405 Method Not Allowed`
_500 = `500 Internal Server Error`
banner = " _ _ _ ___ _ \n | \\| |___| |_ | __|__ _ _ _ _ __| |\n | .` / _ \\ _| | _/ _ \\ || | ' \\/ _` |\n |_|\\_\\___/\\__| |_|\\___/\\_,_|_||_\\__,_|\n "
_404 = "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1\"><meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"><title>Not Found</title><style>body{background:#333;margin:0;color:#ccc;display:flex;align-items:center;max-height:100vh;height:100vh;justify-content:center}textarea{min-height:5rem;min-width:20rem;text-align:center;border:none;background:0 0;color:#ccc;resize:none;user-input:none;user-select:none;cursor:default;-webkit-user-select:none;-webkit-touch-callout:none;-moz-user-select:none;-ms-user-select:none;outline:0}</style></head><body><textarea id=\"banner\" readonly=\"readonly\"></textarea><script type=\"text/javascript\">let htmlCodes = [\n ' _ _ _ ___ _ ',\n '| \\\\| |___| |_ | __|__ _ _ _ _ __| |',\n '| .` / _ \\\\ _| | _/ _ \\\\ || | \\' \\\\/ _` |',\n '|_|\\\\_\\\\___/\\\\__| |_|\\\\___/\\\\_,_|_||_\\\\__,_|'\n].join('\\n');\ndocument.querySelector('#banner').value = htmlCodes</script></body></html>"
_405 = `405 Method Not Allowed`
_500 = `500 Internal Server Error`
TraceKey = "X-Trace-Id"
)
type Map map[string]interface{}

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

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

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

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

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

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

36
nft/nfctl/clone/clone.go Normal file
View File

@ -0,0 +1,36 @@
package clone
import (
"fmt"
"github.com/go-git/go-git/v5"
_ "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/loveuer/nf/nft/log"
"net/url"
)
func Clone(pwd string, ins *url.URL) error {
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,
}
}
log.Info("start clone %s", uri)
_, err := git.PlainClone(pwd, false, opt)
if err != nil {
return err
}
return nil
}

16
nft/nfctl/cmd/check.go Normal file
View File

@ -0,0 +1,16 @@
package cmd
import (
"github.com/loveuer/nf/nft/nfctl/version"
"github.com/spf13/cobra"
)
var (
checkCmd = &cobra.Command{
Use: "check",
Short: "nfctl new version check",
Run: func(cmd *cobra.Command, args []string) {
version.Check(true, true, 30)
},
}
)

30
nft/nfctl/cmd/cmd.go Normal file
View File

@ -0,0 +1,30 @@
package cmd
import (
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/nfctl/opt"
"github.com/spf13/cobra"
)
var (
Root = &cobra.Command{
Use: "nfctl",
Short: "nfctl: easy start your nf backend work",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if opt.Debug == true {
log.SetLogLevel(log.LogLevelDebug)
}
},
}
)
func init() {
initNew()
Root.PersistentFlags().BoolVar(&opt.Debug, "debug", false, "debug mode")
Root.AddCommand(
versionCmd,
checkCmd,
cmdNew,
)
}

135
nft/nfctl/cmd/new.go Normal file
View File

@ -0,0 +1,135 @@
package cmd
import (
"errors"
"fmt"
"net/url"
"os"
"path"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/nfctl/clone"
"github.com/loveuer/nf/nft/nfctl/opt"
"github.com/loveuer/nf/nft/nfctl/tp"
"github.com/loveuer/nf/nft/nfctl/version"
"github.com/spf13/cobra"
)
var (
cmdNew = &cobra.Command{
Use: "new",
Short: "nfctl new: start new project",
Example: `nfctl new {project} -t ultone [recommend]
nfctl new {project} -t https://github.com/loveuer/ultone.git
nfctl new {project} --template http://username:token@my.gitlab.com/my-zone/my-repo.git
`,
SilenceUsage: true,
}
template string
disableInit bool
preTemplateMap = map[string]string{
"ultone": "https://gitcode.com/loveuer/ultone.git",
}
)
func initNew() {
cmdNew.Flags().StringVarP(&template, "template", "t", "", "template name/url[example:ultone, https://github.com/xxx/yyy.git]")
cmdNew.Flags().BoolVar(&disableInit, "without-init", false, "don't run template init script")
cmdNew.RunE = func(cmd *cobra.Command, args []string) error {
version.Check(true, false, 5)
var (
err error
urlIns *url.URL
pwd string
moduleName string
projectDir string
initBs []byte
renderBs []byte
scripts []tp.Cmd
)
if len(args) == 0 {
return fmt.Errorf("project name required")
}
if pwd, err = os.Getwd(); err != nil {
return fmt.Errorf("get work dir err")
}
moduleName = args[0]
projectDir = path.Join(pwd, path.Base(args[0]))
if _, err = os.Stat(projectDir); !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("project folder already exist")
}
if err = os.MkdirAll(projectDir, 0750); err != nil {
return fmt.Errorf("create project dir err: %v", err)
}
defer func() {
if err != nil {
_ = os.RemoveAll(projectDir)
}
}()
if template == "" {
// todo no template new project
return fmt.Errorf("😥create basic project(without template) comming soon...")
}
cloneUrl := template
if ptUrl, ok := preTemplateMap[cloneUrl]; ok {
cloneUrl = ptUrl
}
if urlIns, err = url.Parse(cloneUrl); err != nil {
return fmt.Errorf("invalid clone url: %v", err)
}
if err = clone.Clone(projectDir, urlIns); err != nil {
return fmt.Errorf("clone template err: %v", err)
}
if initBs, err = os.ReadFile(path.Join(projectDir, ".nfctl")); err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return fmt.Errorf("read nfctl script file err: %v", err)
}
if renderBs, err = tp.RenderVar(initBs, map[string]any{
"PROJECT_NAME": projectDir,
"MODULE_NAME": moduleName,
}); err != nil {
return fmt.Errorf("render template init script err: %v", err)
}
if scripts, err = tp.ParseCmd(projectDir, renderBs); err != nil {
return fmt.Errorf("parse template init script err: %v", err)
}
for _, script := range scripts {
if opt.Debug {
log.Debug("start script:\n%s\n", script.String())
}
if err = script.Execute(); err != nil {
return fmt.Errorf("execute template init script err: %v", err)
}
}
if err = os.RemoveAll(path.Join(projectDir, ".git")); err != nil {
log.Warn("remove .git folder err: %s", err.Error())
}
log.Info("🎉 create project [%s] 成功!!!", args[0])
return nil
}
}

18
nft/nfctl/cmd/version.go Normal file
View File

@ -0,0 +1,18 @@
package cmd
import (
"github.com/fatih/color"
"github.com/loveuer/nf/nft/nfctl/version"
"github.com/spf13/cobra"
)
var (
versionCmd = &cobra.Command{
Use: "version",
Short: "print nfctl version and exit",
Run: func(cmd *cobra.Command, args []string) {
color.Cyan("nfctl - version: %s", version.Version)
version.Check(true, false, 5)
},
}
)

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

@ -0,0 +1,19 @@
package main
import (
"context"
"github.com/loveuer/nf/nft/nfctl/cmd"
"os/signal"
"syscall"
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
defer cancel()
//if !(len(os.Args) >= 2 && os.Args[1] == "version") {
// version.Check(5)
//}
_ = cmd.Root.ExecuteContext(ctx)
}

5
nft/nfctl/opt/var.go Normal file
View File

@ -0,0 +1,5 @@
package opt
var (
Debug bool
)

23
nft/nfctl/readme.md Normal file
View File

@ -0,0 +1,23 @@
# nfctl
# 通过 nfctl 快速开启后台项目
### 1. Installation
-`go install github.com/loveuer/nf/nft/nfctl@latest`
- ② download prebuild binary [release](https://github.com/loveuer/nf/releases)
### 2. Usage
- `nfctl new {project}`
- `nfctl new project -t ultone`
- `nfctl new project -t https://github.com/xxx/yyy.git`
- `nfctl new project --template https://gitcode/loveuer/ultone.git`
- `nfctl new project --template https://{username}:{password/token}@my.gitlab.com/name/project.git`
### 3. nfctl init script
- `为方便模版的初始化, 可以采用 nfctl init script, 当 nfctl new project -t xxx 从模版开始项目时会自动执行`
- `具体的编写规则如下:`
* [init 脚本规则](https://github.com/loveuer/nf/nft/nfctl/script.md) 或者
* [国内](https://gitcode.com/loveuer/nf/nft/nfctl/script.md)

80
nft/nfctl/tp/parse.go Normal file
View File

@ -0,0 +1,80 @@
package tp
import (
"bufio"
"bytes"
"fmt"
"strings"
)
func ParseCmd(pwd string, content []byte) ([]Cmd, error) {
var (
err error
cmds = make([]Cmd, 0)
start = false
)
scanner := bufio.NewScanner(bytes.NewReader(content))
scanner.Buffer(make([]byte, 1024), 1024*1024*10)
record := make([]string, 0)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if len(line) == 0 {
continue
}
if !start && strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "!") {
if start {
return nil, fmt.Errorf("invalid content: unEOF cmd block found")
}
start = true
record = append(record, line)
continue
}
if strings.HasPrefix(line, "EOF") {
start = false
if len(record) == 0 {
continue
}
var cmd Cmd
if cmd, err = ParseBlock(pwd, record); err != nil {
return nil, err
}
cmds = append(cmds, cmd)
record = record[:0]
continue
}
if start {
record = append(record, line)
}
}
if err = scanner.Err(); err != nil {
return nil, err
}
return cmds, err
}
func ParseBlock(pwd string, lines []string) (Cmd, error) {
switch lines[0] {
case "!replace content":
return newReplaceContent(pwd, lines[1:])
case "!replace name":
return newReplaceName(pwd, lines[1:])
case "!generate":
return newGenerate(pwd, lines[1:])
}
return nil, fmt.Errorf("invalid cmd block: unknown type: %s", lines[0])
}

View File

@ -0,0 +1,37 @@
package tp
import (
"github.com/loveuer/nf/nft/log"
"os"
"testing"
)
func TestParseInitFile(t *testing.T) {
bs, err := os.ReadFile("xtest")
if err != nil {
log.Fatal(err.Error())
}
data := map[string]any{
"PROJECT_NAME": "myproject",
}
result, err := RenderVar(bs, data)
if err != nil {
log.Fatal(err.Error())
}
pwd, _ := os.Getwd()
cmds, err := ParseCmd(pwd, result)
if err != nil {
log.Fatal(err.Error())
}
for _, item := range cmds {
log.Info("one cmd => %s\n\n", item.String())
if err = item.Execute(); err != nil {
log.Fatal(err.Error())
}
}
}

29
nft/nfctl/tp/render.go Normal file
View File

@ -0,0 +1,29 @@
package tp
import (
"bytes"
"text/template"
)
var (
_t *template.Template
)
func init() {
_t = template.New("tp")
}
func RenderVar(t []byte, data map[string]any) ([]byte, error) {
tr, err := _t.Parse(string(t))
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err = tr.Execute(&buf, data); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

316
nft/nfctl/tp/tp.go Normal file
View File

@ -0,0 +1,316 @@
package tp
import (
"bufio"
"bytes"
"fmt"
"github.com/loveuer/nf/nft/log"
"io/fs"
"os"
"path"
"path/filepath"
"regexp"
"strings"
)
type Cmd interface {
String() string
Execute() error
}
var (
_ Cmd = (*Generate)(nil)
_ Cmd = (*ReplaceContent)(nil)
_ Cmd = (*ReplaceName)(nil)
)
type Generate struct {
pwd string
filename string
content []string
}
func (t *Generate) String() string {
return fmt.Sprintf("!generate\n%s\n%s\n", t.filename, strings.Join(t.content, "\n"))
}
func (t *Generate) Execute() error {
var (
err error
location = t.filename
input *os.File
)
log.Debug("[Generate] generate[%s]", t.filename)
if !path.IsAbs(t.filename) {
location = path.Join(t.pwd, t.filename)
}
if err = os.MkdirAll(path.Dir(location), 0644); err != nil {
return err
}
if !strings.HasSuffix(location, "/") {
if input, err = os.OpenFile(location, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0744); err != nil {
return err
}
if len(t.content) > 0 {
content := strings.Join(t.content, "\n")
_, err = input.WriteString(content)
return err
}
}
return nil
}
func newGenerate(pwd string, lines []string) (*Generate, error) {
if len(lines) == 0 {
return nil, fmt.Errorf("generate cmd require file/folder name")
}
return &Generate{
pwd: pwd,
filename: lines[0],
content: lines[1:],
}, nil
}
type replaceNameMatchType int
const (
replaceNameMatchReg replaceNameMatchType = iota + 1
replaceNameMatchExact
replaceNameMatchPrefix
replaceNameMatchSuffix
)
func (rm replaceNameMatchType) Label() string {
switch rm {
case replaceNameMatchReg:
return "reg"
case replaceNameMatchExact:
return "exact"
case replaceNameMatchPrefix:
return "prefix"
case replaceNameMatchSuffix:
return "suffix"
}
log.Panic("unknown replace match type: %v", rm)
return ""
}
type ReplaceContent struct {
pwd string
name string
content string
targetName string
matchType replaceNameMatchType
fromContent string
targetEmpty bool
targetContent string
}
func (t *ReplaceContent) String() string {
return fmt.Sprintf("!replace content\n%s\n%s\n", t.name, t.content)
}
func (t *ReplaceContent) Execute() error {
var (
fn filepath.WalkFunc
handler = func(location string) error {
bs, err := os.ReadFile(location)
if err != nil {
return err
}
log.Debug("[ReplaceContent] handle[%s] replace [%s] => [%s]", location, t.fromContent, t.targetContent)
newbs, err := t.executeFile(bs)
if err != nil {
return err
}
return os.WriteFile(location, newbs, 0644)
}
)
switch t.matchType {
case replaceNameMatchExact:
fn = func(location string, info fs.FileInfo, err error) error {
if location == path.Join(t.pwd, t.targetName) {
log.Debug("[ReplaceContent] exact match: %s", location)
return handler(location)
}
return nil
}
case replaceNameMatchPrefix:
fn = func(location string, info fs.FileInfo, err error) error {
if strings.HasPrefix(path.Base(location), t.targetName) {
log.Debug("[ReplaceContent] prefix match: %s", location)
return handler(location)
}
return nil
}
case replaceNameMatchSuffix:
fn = func(location string, info fs.FileInfo, err error) error {
if strings.HasSuffix(location, t.targetName) {
log.Debug("[ReplaceContent] suffix match: %s", location)
return handler(location)
}
return nil
}
case replaceNameMatchReg:
fn = func(location string, info fs.FileInfo, err error) error {
if match, err := regexp.MatchString(t.targetName, location); err == nil && match {
log.Debug("[ReplaceContent] reg match: %s", location)
return handler(location)
}
return nil
}
}
return filepath.Walk(t.pwd, fn)
}
func (t *ReplaceContent) executeFile(raw []byte) ([]byte, error) {
scanner := bufio.NewScanner(bytes.NewReader(raw))
scanner.Buffer(make([]byte, 1024), 1024*1024)
lines := make([]string, 0)
for scanner.Scan() {
line := scanner.Text()
lines = append(
lines,
strings.ReplaceAll(line, t.fromContent, t.targetContent),
)
}
return []byte(strings.Join(lines, "\n")), nil
}
func newReplaceContent(pwd string, lines []string) (*ReplaceContent, error) {
if len(lines) != 2 {
return nil, fmt.Errorf("invalid replace_content cmd: required 2 lines params")
}
var (
name = lines[0]
content = lines[1]
matchType replaceNameMatchType
)
names := strings.SplitN(name, " ", 2)
if len(names) != 2 {
return nil, fmt.Errorf("invalid replace_content cmd: name line, required: [reg/exact/prefix/shuffix] {filename}")
}
switch names[0] {
case "exact":
matchType = replaceNameMatchExact
case "reg":
matchType = replaceNameMatchReg
case "prefix":
matchType = replaceNameMatchPrefix
case "suffix":
matchType = replaceNameMatchSuffix
default:
return nil, fmt.Errorf("invalid replace_content name match type, example: [reg *.go] [exact go.mod]")
}
var (
targetName string = names[1]
targetEmpty = false
targetContent string
)
contents := strings.SplitN(content, "=>", 2)
fromContent := strings.TrimSpace(contents[0])
if len(contents) == 1 {
targetEmpty = true
} else {
if targetContent = strings.TrimSpace(contents[1]); targetContent == "" || targetContent == `""` || targetContent == `''` {
targetEmpty = true
}
}
return &ReplaceContent{
pwd: pwd,
name: name,
content: content,
matchType: matchType,
targetName: targetName,
fromContent: fromContent,
targetEmpty: targetEmpty,
targetContent: targetContent,
}, nil
}
type ReplaceName struct {
pwd string
line string
targetEmpty bool
fromContent string
targetContent string
}
func (t *ReplaceName) String() string {
return fmt.Sprintf("!replace name\n%s\n", t.line)
}
func (t *ReplaceName) Execute() error {
fullpath := path.Join(t.pwd, t.fromContent)
if t.targetEmpty {
return os.RemoveAll(fullpath)
}
ftpath := path.Join(t.pwd, t.targetContent)
return os.Rename(fullpath, ftpath)
}
func newReplaceName(pwd string, lines []string) (*ReplaceName, error) {
if len(lines) != 1 {
return nil, fmt.Errorf("replace_name need one line param, for example: mian.go => main.go")
}
var (
content = lines[0]
targetEmpty = false
fromContent string
targetContent string
)
contents := strings.SplitN(content, "=>", 2)
fromContent = strings.TrimSpace(contents[0])
if len(contents) == 1 {
targetEmpty = true
} else {
if targetContent = strings.TrimSpace(contents[1]); targetContent == "" || targetContent == `""` || targetContent == `''` {
targetEmpty = true
}
}
if !targetEmpty {
if (strings.HasPrefix(targetContent, `"`) && strings.HasSuffix(targetContent, `"`)) || (strings.HasPrefix(targetContent, `'`) && strings.HasSuffix(targetContent, `'`)) {
targetContent = targetContent[1 : len(targetContent)-1]
}
}
return &ReplaceName{
pwd: pwd,
line: content,
targetEmpty: targetEmpty,
fromContent: fromContent,
targetContent: targetContent,
}, nil
}

3
nft/nfctl/version/var.go Normal file
View File

@ -0,0 +1,3 @@
package version
const Version = "v24.07.14-r3"

View File

@ -0,0 +1,95 @@
package version
import (
"crypto/tls"
"fmt"
"github.com/fatih/color"
"github.com/loveuer/nf/nft/log"
"github.com/savioxavier/termlink"
"io"
"net/http"
"strings"
"time"
)
var (
uri = "https://raw.gitcode.com/loveuer/nf/raw/master/nft/nfctl/version/var.go"
prefix = "const Version = "
)
func UpgradePrint(newVersion string) {
fmt.Printf(`+----------------------------------------------------------------------+
| 🎉 🎉 🎉 %s 🎉 🎉 🎉 |
| %s |
| Or Download by: |
| %s |
| %s |
+----------------------------------------------------------------------+
`,
color.GreenString("New Version Found: %s", newVersion),
color.CyanString("Upgrade it with: [go install github.com/loveuer/nf/nft/nfctl@master]"),
color.CyanString(termlink.Link("Releases", "https://github.com/loveuer/nf/releases")),
color.CyanString(termlink.Link("Releases", "https://gitcode.com/loveuer/nf/releases")),
)
}
func Check(printUpgradable bool, printNoNeedUpgrade bool, timeouts ...int) string {
var (
v string
)
defer func() {
if printUpgradable {
if v > Version {
UpgradePrint(v)
}
}
if printNoNeedUpgrade {
if v == Version {
color.Cyan("Your Version: %s is Newest", Version)
}
}
}()
timeout := time.Duration(30) * time.Second
if len(timeouts) > 0 && timeouts[0] > 0 {
timeout = time.Duration(timeouts[0]) * time.Second
}
req, _ := http.NewRequest(http.MethodGet, uri, nil)
resp, err := (&http.Client{
Timeout: timeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}).Do(req)
if err != nil {
log.Debug("[Check] http get[%s] err: %v", uri, err.Error())
return ""
}
defer resp.Body.Close()
content, err := io.ReadAll(resp.Body)
if err != nil {
log.Debug("[Check] http read all body err: %v", err)
}
log.Debug("[Check] http get[%s] body:\n%s", uri, string(content))
for _, line := range strings.Split(string(content), "\n") {
log.Debug("[Check] version.go line: %s", line)
if strings.HasPrefix(line, prefix) {
may := strings.TrimPrefix(line, prefix)
if len(may) > 2 {
v = may[1 : len(may)-1]
}
return v
}
}
return ""
}

View File

@ -0,0 +1,17 @@
package version
import (
"github.com/loveuer/nf/nft/log"
"testing"
)
func TestUpgradePrint(t *testing.T) {
log.SetLogLevel(log.LogLevelDebug)
UpgradePrint("v24.07.14-r5")
}
func TestCheck(t *testing.T) {
log.SetLogLevel(log.LogLevelDebug)
v := Check(true, true, 1)
t.Logf("got version: %s", v)
}

View File

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

View File

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

135
readme.md
View File

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

View File

@ -42,8 +42,9 @@ type ResponseWriter interface {
type responseWriter struct {
http.ResponseWriter
size int
status int
written bool
size int
status int
}
var _ ResponseWriter = (*responseWriter)(nil)
@ -103,7 +104,7 @@ func (w *responseWriter) Size() int {
}
func (w *responseWriter) Written() bool {
return w.size != noWritten || w.status != 0
return w.size != noWritten || w.written
}
// Hijack implements the http.Hijacker interface.

21
util.go
View File

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