admin / context /context.go
AZLABS's picture
Upload folder using huggingface_hub
530729e verified
// Copyright 2019 GoAdmin Core Team. All rights reserved.
// Use of this source code is governed by a Apache-2.0 style
// license that can be found in the LICENSE file.
package context
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net"
"net/http"
"net/url"
"os"
"path"
"strings"
"time"
"github.com/GoAdminGroup/go-admin/modules/constant"
)
const abortIndex int8 = math.MaxInt8 / 2
// Context is the simplify version of web framework context.
// But it is important which will be used in plugins to custom
// the request and response. And adapter will help to transform
// the Context to the web framework`s context. It has three attributes.
// Request and response are belongs to net/http package. UserValue
// is the custom key-value store of context.
type Context struct {
Request *http.Request
Response *http.Response
UserValue map[string]interface{}
index int8
handlers Handlers
}
// Path is used in the matching of request and response. Url stores the
// raw register url. RegUrl contains the wildcard which on behalf of
// the route params.
type Path struct {
URL string
Method string
}
type RouterMap map[string]Router
func (r RouterMap) Get(name string) Router {
return r[name]
}
type Router struct {
Methods []string
Patten string
}
func (r Router) Method() string {
return r.Methods[0]
}
func (r Router) GetURL(value ...string) string {
u := r.Patten
for i := 0; i < len(value); i += 2 {
u = strings.ReplaceAll(u, ":__"+value[i], value[i+1])
}
return u
}
type NodeProcessor func(...Node)
type Node struct {
Path string
Method string
Handlers []Handler
Value map[string]interface{}
}
// SetUserValue set the value of user context.
func (ctx *Context) SetUserValue(key string, value interface{}) {
ctx.UserValue[key] = value
}
// GetUserValue get the value of key.
func (ctx *Context) GetUserValue(key string) interface{} {
return ctx.UserValue[key]
}
// Path return the url path.
func (ctx *Context) Path() string {
return ctx.Request.URL.Path
}
// Abort abort the context.
func (ctx *Context) Abort() {
ctx.index = abortIndex
}
// Next should be used only inside middleware.
func (ctx *Context) Next() {
ctx.index++
for s := int8(len(ctx.handlers)); ctx.index < s; ctx.index++ {
ctx.handlers[ctx.index](ctx)
}
}
// SetHandlers set the handlers of Context.
func (ctx *Context) SetHandlers(handlers Handlers) *Context {
ctx.handlers = handlers
return ctx
}
// Method return the request method.
func (ctx *Context) Method() string {
return ctx.Request.Method
}
// NewContext used in adapter which return a Context with request
// and slice of UserValue and a default Response.
func NewContext(req *http.Request) *Context {
return &Context{
Request: req,
UserValue: make(map[string]interface{}),
Response: &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
},
index: -1,
}
}
const (
HeaderContentType = "Content-Type"
HeaderLastModified = "Last-Modified"
HeaderIfModifiedSince = "If-Modified-Since"
HeaderCacheControl = "Cache-Control"
HeaderETag = "ETag"
HeaderContentDisposition = "Content-Disposition"
HeaderContentLength = "Content-Length"
HeaderContentEncoding = "Content-Encoding"
GzipHeaderValue = "gzip"
HeaderAcceptEncoding = "Accept-Encoding"
HeaderVary = "Vary"
ThemeKey = "__ga_theme"
)
func (ctx *Context) BindJSON(data interface{}) error {
if ctx.Request.Body != nil {
b, err := io.ReadAll(ctx.Request.Body)
if err == nil {
return json.Unmarshal(b, data)
}
return err
}
return errors.New("empty request body")
}
func (ctx *Context) MustBindJSON(data interface{}) {
if ctx.Request.Body != nil {
b, err := io.ReadAll(ctx.Request.Body)
if err != nil {
panic(err)
}
err = json.Unmarshal(b, data)
if err != nil {
panic(err)
}
}
panic("empty request body")
}
// Write save the given status code, headers and body string into the response.
func (ctx *Context) Write(code int, header map[string]string, Body string) {
ctx.Response.StatusCode = code
for key, head := range header {
ctx.AddHeader(key, head)
}
ctx.Response.Body = io.NopCloser(strings.NewReader(Body))
}
// JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json".
func (ctx *Context) JSON(code int, Body map[string]interface{}) {
ctx.Response.StatusCode = code
ctx.SetContentType("application/json")
BodyStr, err := json.Marshal(Body)
if err != nil {
panic(err)
}
ctx.Response.Body = io.NopCloser(bytes.NewReader(BodyStr))
}
// DataWithHeaders save the given status code, headers and body data into the response.
func (ctx *Context) DataWithHeaders(code int, header map[string]string, data []byte) {
ctx.Response.StatusCode = code
for key, head := range header {
ctx.AddHeader(key, head)
}
ctx.Response.Body = io.NopCloser(bytes.NewBuffer(data))
}
// Data writes some data into the body stream and updates the HTTP code.
func (ctx *Context) Data(code int, contentType string, data []byte) {
ctx.Response.StatusCode = code
ctx.SetContentType(contentType)
ctx.Response.Body = io.NopCloser(bytes.NewBuffer(data))
}
// Redirect add redirect url to header.
func (ctx *Context) Redirect(path string) {
ctx.Response.StatusCode = http.StatusFound
ctx.SetContentType("text/html; charset=utf-8")
ctx.AddHeader("Location", path)
}
// HTML output html response.
func (ctx *Context) HTML(code int, body string) {
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(code)
ctx.WriteString(body)
}
// HTMLByte output html response.
func (ctx *Context) HTMLByte(code int, body []byte) {
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(code)
ctx.Response.Body = io.NopCloser(bytes.NewBuffer(body))
}
// WriteString save the given body string into the response.
func (ctx *Context) WriteString(body string) {
ctx.Response.Body = io.NopCloser(strings.NewReader(body))
}
// SetStatusCode save the given status code into the response.
func (ctx *Context) SetStatusCode(code int) {
ctx.Response.StatusCode = code
}
// SetContentType save the given content type header into the response header.
func (ctx *Context) SetContentType(contentType string) {
ctx.AddHeader(HeaderContentType, contentType)
}
func (ctx *Context) SetLastModified(modtime time.Time) {
if !IsZeroTime(modtime) {
ctx.AddHeader(HeaderLastModified, modtime.UTC().Format(http.TimeFormat)) // or modtime.UTC()?
}
}
var unixEpochTime = time.Unix(0, 0)
// IsZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
func IsZeroTime(t time.Time) bool {
return t.IsZero() || t.Equal(unixEpochTime)
}
// ParseTime parses a time header (such as the Date: header),
// trying each forth formats
// that are allowed by HTTP/1.1:
// time.RFC850, and time.ANSIC.
var ParseTime = func(text string) (t time.Time, err error) {
t, err = time.Parse(http.TimeFormat, text)
if err != nil {
return http.ParseTime(text)
}
return
}
func (ctx *Context) WriteNotModified() {
// RFC 7232 section 4.1:
// a sender SHOULD NOT generate representation metadata other than the
// above listed fields unless said metadata exists for the purpose of
// guiding cache updates (e.g.," Last-Modified" might be useful if the
// response does not have an ETag field).
delete(ctx.Response.Header, HeaderContentType)
delete(ctx.Response.Header, HeaderContentLength)
if ctx.Headers(HeaderETag) != "" {
delete(ctx.Response.Header, HeaderLastModified)
}
ctx.SetStatusCode(http.StatusNotModified)
}
func (ctx *Context) CheckIfModifiedSince(modtime time.Time) (bool, error) {
if method := ctx.Method(); method != http.MethodGet && method != http.MethodHead {
return false, errors.New("skip: method")
}
ims := ctx.Headers(HeaderIfModifiedSince)
if ims == "" || IsZeroTime(modtime) {
return false, errors.New("skip: zero time")
}
t, err := ParseTime(ims)
if err != nil {
return false, errors.New("skip: " + err.Error())
}
// sub-second precision, so
// use mtime < t+1s instead of mtime <= t to check for unmodified.
if modtime.UTC().Before(t.Add(1 * time.Second)) {
return false, nil
}
return true, nil
}
// LocalIP return the request client ip.
func (ctx *Context) LocalIP() string {
xForwardedFor := ctx.Request.Header.Get("X-Forwarded-For")
ip := strings.TrimSpace(strings.Split(xForwardedFor, ",")[0])
if ip != "" {
return ip
}
ip = strings.TrimSpace(ctx.Request.Header.Get("X-Real-Ip"))
if ip != "" {
return ip
}
if ip, _, err := net.SplitHostPort(strings.TrimSpace(ctx.Request.RemoteAddr)); err == nil {
return ip
}
return "127.0.0.1"
}
// SetCookie save the given cookie obj into the response Set-Cookie header.
func (ctx *Context) SetCookie(cookie *http.Cookie) {
if v := cookie.String(); v != "" {
ctx.AddHeader("Set-Cookie", v)
}
}
// Query get the query parameter of url.
func (ctx *Context) Query(key string) string {
return ctx.Request.URL.Query().Get(key)
}
// QueryAll get the query parameters of url.
func (ctx *Context) QueryAll(key string) []string {
return ctx.Request.URL.Query()[key]
}
// QueryDefault get the query parameter of url. If it is empty, return the default.
func (ctx *Context) QueryDefault(key, def string) string {
value := ctx.Query(key)
if value == "" {
return def
}
return value
}
// Lang get the query parameter of url with given key __ga_lang.
func (ctx *Context) Lang() string {
return ctx.Query("__ga_lang")
}
// Theme get the request theme with given key __ga_theme.
func (ctx *Context) Theme() string {
queryTheme := ctx.Query(ThemeKey)
if queryTheme != "" {
return queryTheme
}
cookieTheme := ctx.Cookie(ThemeKey)
if cookieTheme != "" {
return cookieTheme
}
return ctx.RefererQuery(ThemeKey)
}
// Headers get the value of request headers key.
func (ctx *Context) Headers(key string) string {
return ctx.Request.Header.Get(key)
}
// Referer get the url string of request header Referer.
func (ctx *Context) Referer() string {
return ctx.Headers("Referer")
}
// RefererURL get the url.URL object of request header Referer.
func (ctx *Context) RefererURL() *url.URL {
ref := ctx.Headers("Referer")
if ref == "" {
return nil
}
u, err := url.Parse(ref)
if err != nil {
return nil
}
return u
}
// RefererQuery retrieve the value of given key from url.URL object of request header Referer.
func (ctx *Context) RefererQuery(key string) string {
if u := ctx.RefererURL(); u != nil {
return u.Query().Get(key)
}
return ""
}
// FormValue get the value of request form key.
func (ctx *Context) FormValue(key string) string {
return ctx.Request.FormValue(key)
}
// PostForm get the values of request form.
func (ctx *Context) PostForm() url.Values {
_ = ctx.Request.ParseMultipartForm(32 << 20)
return ctx.Request.PostForm
}
func (ctx *Context) WantHTML() bool {
return ctx.Method() == "GET" && strings.Contains(ctx.Headers("Accept"), "html")
}
func (ctx *Context) WantJSON() bool {
return strings.Contains(ctx.Headers("Accept"), "json")
}
// AddHeader adds the key, value pair to the header.
func (ctx *Context) AddHeader(key, value string) {
ctx.Response.Header.Add(key, value)
}
// PjaxUrl add pjax url header.
func (ctx *Context) PjaxUrl(url string) {
ctx.Response.Header.Add(constant.PjaxUrlHeader, url)
}
// IsPjax check request is pjax or not.
func (ctx *Context) IsPjax() bool {
return ctx.Headers(constant.PjaxHeader) == "true"
}
// IsIframe check request is iframe or not.
func (ctx *Context) IsIframe() bool {
return ctx.Query(constant.IframeKey) == "true" || ctx.Headers(constant.IframeKey) == "true"
}
// SetHeader set the key, value pair to the header.
func (ctx *Context) SetHeader(key, value string) {
ctx.Response.Header.Set(key, value)
}
func (ctx *Context) GetContentType() string {
return ctx.Request.Header.Get("Content-Type")
}
func (ctx *Context) Cookie(name string) string {
for _, ck := range ctx.Request.Cookies() {
if ck.Name == name {
return ck.Value
}
}
return ""
}
// User return the current login user.
func (ctx *Context) User() interface{} {
return ctx.UserValue["user"]
}
// ServeContent serves content, headers are autoset
// receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string)
//
// You can define your own "Content-Type" header also, after this function call
// Doesn't implements resuming (by range), use ctx.SendFile instead
func (ctx *Context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error {
if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil {
ctx.WriteNotModified()
return nil
}
if ctx.GetContentType() == "" {
ctx.SetContentType(filename)
}
buf, _ := io.ReadAll(content)
ctx.Response.Body = io.NopCloser(bytes.NewBuffer(buf))
return nil
}
// ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename)
func (ctx *Context) ServeFile(filename string, gzipCompression bool) error {
f, err := os.Open(filename)
if err != nil {
return fmt.Errorf("%d", http.StatusNotFound)
}
defer func() {
_ = f.Close()
}()
fi, _ := f.Stat()
if fi.IsDir() {
return ctx.ServeFile(path.Join(filename, "index.html"), gzipCompression)
}
return ctx.ServeContent(f, fi.Name(), fi.ModTime(), gzipCompression)
}
type HandlerMap map[Path]Handlers
// App is the key struct of the package. App as a member of plugin
// entity contains the request and the corresponding handler. Prefix
// is the url prefix and MiddlewareList is for control flow.
type App struct {
Requests []Path
Handlers HandlerMap
Middlewares Handlers
Prefix string
Routers RouterMap
routeIndex int
routeANY bool
}
// NewApp return an empty app.
func NewApp() *App {
return &App{
Requests: make([]Path, 0),
Handlers: make(HandlerMap),
Prefix: "/",
Middlewares: make([]Handler, 0),
routeIndex: -1,
Routers: make(RouterMap),
}
}
// Handler defines the handler used by the middleware as return value.
type Handler func(ctx *Context)
// Handlers is the array of Handler
type Handlers []Handler
// AppendReqAndResp stores the request info and handle into app.
// support the route parameter. The route parameter will be recognized as
// wildcard store into the RegUrl of Path struct. For example:
//
// /user/:id => /user/(.*)
// /user/:id/info => /user/(.*?)/info
//
// The RegUrl will be used to recognize the incoming path and find
// the handler.
func (app *App) AppendReqAndResp(url, method string, handler []Handler) {
app.Requests = append(app.Requests, Path{
URL: join(app.Prefix, url),
Method: method,
})
app.routeIndex++
app.Handlers[Path{
URL: join(app.Prefix, url),
Method: method,
}] = append(app.Middlewares, handler...)
}
// Find is public helper method for findPath of tree.
func (app *App) Find(url, method string) []Handler {
app.routeANY = false
return app.Handlers[Path{URL: url, Method: method}]
}
// POST is a shortcut for app.AppendReqAndResp(url, "post", handler).
func (app *App) POST(url string, handler ...Handler) *App {
app.routeANY = false
app.AppendReqAndResp(url, "post", handler)
return app
}
// GET is a shortcut for app.AppendReqAndResp(url, "get", handler).
func (app *App) GET(url string, handler ...Handler) *App {
app.routeANY = false
app.AppendReqAndResp(url, "get", handler)
return app
}
// DELETE is a shortcut for app.AppendReqAndResp(url, "delete", handler).
func (app *App) DELETE(url string, handler ...Handler) *App {
app.routeANY = false
app.AppendReqAndResp(url, "delete", handler)
return app
}
// PUT is a shortcut for app.AppendReqAndResp(url, "put", handler).
func (app *App) PUT(url string, handler ...Handler) *App {
app.routeANY = false
app.AppendReqAndResp(url, "put", handler)
return app
}
// OPTIONS is a shortcut for app.AppendReqAndResp(url, "options", handler).
func (app *App) OPTIONS(url string, handler ...Handler) *App {
app.routeANY = false
app.AppendReqAndResp(url, "options", handler)
return app
}
// HEAD is a shortcut for app.AppendReqAndResp(url, "head", handler).
func (app *App) HEAD(url string, handler ...Handler) *App {
app.routeANY = false
app.AppendReqAndResp(url, "head", handler)
return app
}
// ANY registers a route that matches all the HTTP methods.
// GET, POST, PUT, HEAD, OPTIONS, DELETE.
func (app *App) ANY(url string, handler ...Handler) *App {
app.routeANY = true
app.AppendReqAndResp(url, "post", handler)
app.AppendReqAndResp(url, "get", handler)
app.AppendReqAndResp(url, "delete", handler)
app.AppendReqAndResp(url, "put", handler)
app.AppendReqAndResp(url, "options", handler)
app.AppendReqAndResp(url, "head", handler)
return app
}
func (app *App) Name(name string) {
if app.routeANY {
app.Routers[name] = Router{
Methods: []string{"POST", "GET", "DELETE", "PUT", "OPTIONS", "HEAD"},
Patten: app.Requests[app.routeIndex].URL,
}
} else {
app.Routers[name] = Router{
Methods: []string{app.Requests[app.routeIndex].Method},
Patten: app.Requests[app.routeIndex].URL,
}
}
}
// Group add middlewares and prefix for App.
func (app *App) Group(prefix string, middleware ...Handler) *RouterGroup {
return &RouterGroup{
app: app,
Middlewares: append(app.Middlewares, middleware...),
Prefix: slash(prefix),
}
}
// RouterGroup is a group of routes.
type RouterGroup struct {
app *App
Middlewares Handlers
Prefix string
}
// AppendReqAndResp stores the request info and handle into app.
// support the route parameter. The route parameter will be recognized as
// wildcard store into the RegUrl of Path struct. For example:
//
// /user/:id => /user/(.*)
// /user/:id/info => /user/(.*?)/info
//
// The RegUrl will be used to recognize the incoming path and find
// the handler.
func (g *RouterGroup) AppendReqAndResp(url, method string, handler []Handler) {
g.app.Requests = append(g.app.Requests, Path{
URL: join(g.Prefix, url),
Method: method,
})
g.app.routeIndex++
var h = make([]Handler, len(g.Middlewares))
copy(h, g.Middlewares)
g.app.Handlers[Path{
URL: join(g.Prefix, url),
Method: method,
}] = append(h, handler...)
}
// POST is a shortcut for app.AppendReqAndResp(url, "post", handler).
func (g *RouterGroup) POST(url string, handler ...Handler) *RouterGroup {
g.app.routeANY = false
g.AppendReqAndResp(url, "post", handler)
return g
}
// GET is a shortcut for app.AppendReqAndResp(url, "get", handler).
func (g *RouterGroup) GET(url string, handler ...Handler) *RouterGroup {
g.app.routeANY = false
g.AppendReqAndResp(url, "get", handler)
return g
}
// DELETE is a shortcut for app.AppendReqAndResp(url, "delete", handler).
func (g *RouterGroup) DELETE(url string, handler ...Handler) *RouterGroup {
g.app.routeANY = false
g.AppendReqAndResp(url, "delete", handler)
return g
}
// PUT is a shortcut for app.AppendReqAndResp(url, "put", handler).
func (g *RouterGroup) PUT(url string, handler ...Handler) *RouterGroup {
g.app.routeANY = false
g.AppendReqAndResp(url, "put", handler)
return g
}
// OPTIONS is a shortcut for app.AppendReqAndResp(url, "options", handler).
func (g *RouterGroup) OPTIONS(url string, handler ...Handler) *RouterGroup {
g.app.routeANY = false
g.AppendReqAndResp(url, "options", handler)
return g
}
// HEAD is a shortcut for app.AppendReqAndResp(url, "head", handler).
func (g *RouterGroup) HEAD(url string, handler ...Handler) *RouterGroup {
g.app.routeANY = false
g.AppendReqAndResp(url, "head", handler)
return g
}
// ANY registers a route that matches all the HTTP methods.
// GET, POST, PUT, HEAD, OPTIONS, DELETE.
func (g *RouterGroup) ANY(url string, handler ...Handler) *RouterGroup {
g.app.routeANY = true
g.AppendReqAndResp(url, "post", handler)
g.AppendReqAndResp(url, "get", handler)
g.AppendReqAndResp(url, "delete", handler)
g.AppendReqAndResp(url, "put", handler)
g.AppendReqAndResp(url, "options", handler)
g.AppendReqAndResp(url, "head", handler)
return g
}
func (g *RouterGroup) Name(name string) {
g.app.Name(name)
}
// Group add middlewares and prefix for RouterGroup.
func (g *RouterGroup) Group(prefix string, middleware ...Handler) *RouterGroup {
return &RouterGroup{
app: g.app,
Middlewares: append(g.Middlewares, middleware...),
Prefix: join(slash(g.Prefix), slash(prefix)),
}
}
// slash fix the path which has wrong format problem.
//
// "" => "/"
// "abc/" => "/abc"
// "/abc/" => "/abc"
// "/abc" => "/abc"
// "/" => "/"
func slash(prefix string) string {
prefix = strings.TrimSpace(prefix)
if prefix == "" || prefix == "/" {
return "/"
}
if prefix[0] != '/' {
if prefix[len(prefix)-1] == '/' {
return "/" + prefix[:len(prefix)-1]
}
return "/" + prefix
}
if prefix[len(prefix)-1] == '/' {
return prefix[:len(prefix)-1]
}
return prefix
}
// join join the path.
func join(prefix, suffix string) string {
if prefix == "/" {
return suffix
}
if suffix == "/" {
return prefix
}
return prefix + suffix
}