github.com/kotovmak/go-admin@v1.1.1/context/context.go (about) 1 // Copyright 2019 GoAdmin Core Team. All rights reserved. 2 // Use of this source code is governed by a Apache-2.0 style 3 // license that can be found in the LICENSE file. 4 5 package context 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "io" 13 "io/ioutil" 14 "math" 15 "net" 16 "net/http" 17 "net/url" 18 "os" 19 "path" 20 "strings" 21 "time" 22 23 "github.com/kotovmak/go-admin/modules/constant" 24 ) 25 26 const abortIndex int8 = math.MaxInt8 / 2 27 28 // Context is the simplify version of web framework context. 29 // But it is important which will be used in plugins to custom 30 // the request and response. And adapter will help to transform 31 // the Context to the web framework`s context. It has three attributes. 32 // Request and response are belongs to net/http package. UserValue 33 // is the custom key-value store of context. 34 type Context struct { 35 Request *http.Request 36 Response *http.Response 37 UserValue map[string]interface{} 38 index int8 39 handlers Handlers 40 } 41 42 // Path is used in the matching of request and response. Url stores the 43 // raw register url. RegUrl contains the wildcard which on behalf of 44 // the route params. 45 type Path struct { 46 URL string 47 Method string 48 } 49 50 type RouterMap map[string]Router 51 52 func (r RouterMap) Get(name string) Router { 53 return r[name] 54 } 55 56 type Router struct { 57 Methods []string 58 Patten string 59 } 60 61 func (r Router) Method() string { 62 return r.Methods[0] 63 } 64 65 func (r Router) GetURL(value ...string) string { 66 u := r.Patten 67 for i := 0; i < len(value); i += 2 { 68 u = strings.ReplaceAll(u, ":__"+value[i], value[i+1]) 69 } 70 return u 71 } 72 73 type NodeProcessor func(...Node) 74 75 type Node struct { 76 Path string 77 Method string 78 Handlers []Handler 79 Value map[string]interface{} 80 } 81 82 // SetUserValue set the value of user context. 83 func (ctx *Context) SetUserValue(key string, value interface{}) { 84 ctx.UserValue[key] = value 85 } 86 87 // Path return the url path. 88 func (ctx *Context) Path() string { 89 return ctx.Request.URL.Path 90 } 91 92 // Abort abort the context. 93 func (ctx *Context) Abort() { 94 ctx.index = abortIndex 95 } 96 97 // Next should be used only inside middleware. 98 func (ctx *Context) Next() { 99 ctx.index++ 100 for s := int8(len(ctx.handlers)); ctx.index < s; ctx.index++ { 101 ctx.handlers[ctx.index](ctx) 102 } 103 } 104 105 // SetHandlers set the handlers of Context. 106 func (ctx *Context) SetHandlers(handlers Handlers) *Context { 107 ctx.handlers = handlers 108 return ctx 109 } 110 111 // Method return the request method. 112 func (ctx *Context) Method() string { 113 return ctx.Request.Method 114 } 115 116 // NewContext used in adapter which return a Context with request 117 // and slice of UserValue and a default Response. 118 func NewContext(req *http.Request) *Context { 119 120 return &Context{ 121 Request: req, 122 UserValue: make(map[string]interface{}), 123 Response: &http.Response{ 124 StatusCode: http.StatusOK, 125 Header: make(http.Header), 126 }, 127 index: -1, 128 } 129 } 130 131 const ( 132 HeaderContentType = "Content-Type" 133 134 HeaderLastModified = "Last-Modified" 135 HeaderIfModifiedSince = "If-Modified-Since" 136 HeaderCacheControl = "Cache-Control" 137 HeaderETag = "ETag" 138 139 HeaderContentDisposition = "Content-Disposition" 140 HeaderContentLength = "Content-Length" 141 HeaderContentEncoding = "Content-Encoding" 142 143 GzipHeaderValue = "gzip" 144 HeaderAcceptEncoding = "Accept-Encoding" 145 HeaderVary = "Vary" 146 ) 147 148 func (ctx *Context) BindJSON(data interface{}) error { 149 if ctx.Request.Body != nil { 150 b, err := ioutil.ReadAll(ctx.Request.Body) 151 if err == nil { 152 return json.Unmarshal(b, data) 153 } 154 return err 155 } 156 return errors.New("empty request body") 157 } 158 159 func (ctx *Context) MustBindJSON(data interface{}) { 160 if ctx.Request.Body != nil { 161 b, err := ioutil.ReadAll(ctx.Request.Body) 162 if err != nil { 163 panic(err) 164 } 165 err = json.Unmarshal(b, data) 166 if err != nil { 167 panic(err) 168 } 169 } 170 panic("empty request body") 171 } 172 173 // Write save the given status code, headers and body string into the response. 174 func (ctx *Context) Write(code int, header map[string]string, Body string) { 175 ctx.Response.StatusCode = code 176 for key, head := range header { 177 ctx.AddHeader(key, head) 178 } 179 ctx.Response.Body = ioutil.NopCloser(strings.NewReader(Body)) 180 } 181 182 // JSON serializes the given struct as JSON into the response body. 183 // It also sets the Content-Type as "application/json". 184 func (ctx *Context) JSON(code int, Body map[string]interface{}) { 185 ctx.Response.StatusCode = code 186 ctx.SetContentType("application/json") 187 BodyStr, err := json.Marshal(Body) 188 if err != nil { 189 panic(err) 190 } 191 ctx.Response.Body = ioutil.NopCloser(bytes.NewReader(BodyStr)) 192 } 193 194 // DataWithHeaders save the given status code, headers and body data into the response. 195 func (ctx *Context) DataWithHeaders(code int, header map[string]string, data []byte) { 196 ctx.Response.StatusCode = code 197 for key, head := range header { 198 ctx.AddHeader(key, head) 199 } 200 ctx.Response.Body = ioutil.NopCloser(bytes.NewBuffer(data)) 201 } 202 203 // Data writes some data into the body stream and updates the HTTP code. 204 func (ctx *Context) Data(code int, contentType string, data []byte) { 205 ctx.Response.StatusCode = code 206 ctx.SetContentType(contentType) 207 ctx.Response.Body = ioutil.NopCloser(bytes.NewBuffer(data)) 208 } 209 210 // Redirect add redirect url to header. 211 func (ctx *Context) Redirect(path string) { 212 ctx.Response.StatusCode = http.StatusFound 213 ctx.SetContentType("text/html; charset=utf-8") 214 ctx.AddHeader("Location", path) 215 } 216 217 // HTML output html response. 218 func (ctx *Context) HTML(code int, body string) { 219 ctx.SetContentType("text/html; charset=utf-8") 220 ctx.SetStatusCode(code) 221 ctx.WriteString(body) 222 } 223 224 // HTMLByte output html response. 225 func (ctx *Context) HTMLByte(code int, body []byte) { 226 ctx.SetContentType("text/html; charset=utf-8") 227 ctx.SetStatusCode(code) 228 ctx.Response.Body = ioutil.NopCloser(bytes.NewBuffer(body)) 229 } 230 231 // WriteString save the given body string into the response. 232 func (ctx *Context) WriteString(body string) { 233 ctx.Response.Body = ioutil.NopCloser(strings.NewReader(body)) 234 } 235 236 // SetStatusCode save the given status code into the response. 237 func (ctx *Context) SetStatusCode(code int) { 238 ctx.Response.StatusCode = code 239 } 240 241 // SetContentType save the given content type header into the response header. 242 func (ctx *Context) SetContentType(contentType string) { 243 ctx.AddHeader(HeaderContentType, contentType) 244 } 245 246 func (ctx *Context) SetLastModified(modtime time.Time) { 247 if !IsZeroTime(modtime) { 248 ctx.AddHeader(HeaderLastModified, modtime.UTC().Format(http.TimeFormat)) // or modtime.UTC()? 249 } 250 } 251 252 var unixEpochTime = time.Unix(0, 0) 253 254 // IsZeroTime reports whether t is obviously unspecified (either zero or Unix()=0). 255 func IsZeroTime(t time.Time) bool { 256 return t.IsZero() || t.Equal(unixEpochTime) 257 } 258 259 // ParseTime parses a time header (such as the Date: header), 260 // trying each forth formats 261 // that are allowed by HTTP/1.1: 262 // time.RFC850, and time.ANSIC. 263 var ParseTime = func(text string) (t time.Time, err error) { 264 t, err = time.Parse(http.TimeFormat, text) 265 if err != nil { 266 return http.ParseTime(text) 267 } 268 269 return 270 } 271 272 func (ctx *Context) WriteNotModified() { 273 // RFC 7232 section 4.1: 274 // a sender SHOULD NOT generate representation metadata other than the 275 // above listed fields unless said metadata exists for the purpose of 276 // guiding cache updates (e.g.," Last-Modified" might be useful if the 277 // response does not have an ETag field). 278 delete(ctx.Response.Header, HeaderContentType) 279 delete(ctx.Response.Header, HeaderContentLength) 280 if ctx.Headers(HeaderETag) != "" { 281 delete(ctx.Response.Header, HeaderLastModified) 282 } 283 ctx.SetStatusCode(http.StatusNotModified) 284 } 285 286 func (ctx *Context) CheckIfModifiedSince(modtime time.Time) (bool, error) { 287 if method := ctx.Method(); method != http.MethodGet && method != http.MethodHead { 288 return false, errors.New("skip: method") 289 } 290 ims := ctx.Headers(HeaderIfModifiedSince) 291 if ims == "" || IsZeroTime(modtime) { 292 return false, errors.New("skip: zero time") 293 } 294 t, err := ParseTime(ims) 295 if err != nil { 296 return false, errors.New("skip: " + err.Error()) 297 } 298 // sub-second precision, so 299 // use mtime < t+1s instead of mtime <= t to check for unmodified. 300 if modtime.UTC().Before(t.Add(1 * time.Second)) { 301 return false, nil 302 } 303 return true, nil 304 } 305 306 // LocalIP return the request client ip. 307 func (ctx *Context) LocalIP() string { 308 xForwardedFor := ctx.Request.Header.Get("X-Forwarded-For") 309 ip := strings.TrimSpace(strings.Split(xForwardedFor, ",")[0]) 310 if ip != "" { 311 return ip 312 } 313 314 ip = strings.TrimSpace(ctx.Request.Header.Get("X-Real-Ip")) 315 if ip != "" { 316 return ip 317 } 318 319 if ip, _, err := net.SplitHostPort(strings.TrimSpace(ctx.Request.RemoteAddr)); err == nil { 320 return ip 321 } 322 323 return "127.0.0.1" 324 } 325 326 // SetCookie save the given cookie obj into the response Set-Cookie header. 327 func (ctx *Context) SetCookie(cookie *http.Cookie) { 328 if v := cookie.String(); v != "" { 329 ctx.AddHeader("Set-Cookie", v) 330 } 331 } 332 333 // Query get the query parameter of url. 334 func (ctx *Context) Query(key string) string { 335 return ctx.Request.URL.Query().Get(key) 336 } 337 338 // QueryAll get the query parameters of url. 339 func (ctx *Context) QueryAll(key string) []string { 340 return ctx.Request.URL.Query()[key] 341 } 342 343 // QueryDefault get the query parameter of url. If it is empty, return the default. 344 func (ctx *Context) QueryDefault(key, def string) string { 345 value := ctx.Query(key) 346 if value == "" { 347 return def 348 } 349 return value 350 } 351 352 // Lang get the query parameter of url with given key __ga_lang. 353 func (ctx *Context) Lang() string { 354 return ctx.Query("__ga_lang") 355 } 356 357 // Headers get the value of request headers key. 358 func (ctx *Context) Headers(key string) string { 359 return ctx.Request.Header.Get(key) 360 } 361 362 // Referer get the url string of request header Referer. 363 func (ctx *Context) Referer() string { 364 return ctx.Headers("Referer") 365 } 366 367 // RefererURL get the url.URL object of request header Referer. 368 func (ctx *Context) RefererURL() *url.URL { 369 ref := ctx.Headers("Referer") 370 if ref == "" { 371 return nil 372 } 373 u, err := url.Parse(ref) 374 if err != nil { 375 return nil 376 } 377 return u 378 } 379 380 // RefererQuery retrieve the value of given key from url.URL object of request header Referer. 381 func (ctx *Context) RefererQuery(key string) string { 382 if u := ctx.RefererURL(); u != nil { 383 return u.Query().Get(key) 384 } 385 return "" 386 } 387 388 // FormValue get the value of request form key. 389 func (ctx *Context) FormValue(key string) string { 390 return ctx.Request.FormValue(key) 391 } 392 393 // PostForm get the values of request form. 394 func (ctx *Context) PostForm() url.Values { 395 _ = ctx.Request.ParseMultipartForm(32 << 20) 396 return ctx.Request.PostForm 397 } 398 399 func (ctx *Context) WantHTML() bool { 400 return ctx.Method() == "GET" && strings.Contains(ctx.Headers("Accept"), "html") 401 } 402 403 func (ctx *Context) WantJSON() bool { 404 return strings.Contains(ctx.Headers("Accept"), "json") 405 } 406 407 // AddHeader adds the key, value pair to the header. 408 func (ctx *Context) AddHeader(key, value string) { 409 ctx.Response.Header.Add(key, value) 410 } 411 412 // PjaxUrl add pjax url header. 413 func (ctx *Context) PjaxUrl(url string) { 414 ctx.Response.Header.Add(constant.PjaxUrlHeader, url) 415 } 416 417 // IsPjax check request is pjax or not. 418 func (ctx *Context) IsPjax() bool { 419 return ctx.Headers(constant.PjaxHeader) == "true" 420 } 421 422 // IsIframe check request is iframe or not. 423 func (ctx *Context) IsIframe() bool { 424 return ctx.Query(constant.IframeKey) == "true" || ctx.Headers(constant.IframeKey) == "true" 425 } 426 427 // SetHeader set the key, value pair to the header. 428 func (ctx *Context) SetHeader(key, value string) { 429 ctx.Response.Header.Set(key, value) 430 } 431 432 func (ctx *Context) GetContentType() string { 433 return ctx.Request.Header.Get("Content-Type") 434 } 435 436 func (ctx *Context) Cookie(name string) string { 437 for _, ck := range ctx.Request.Cookies() { 438 if ck.Name == name { 439 return ck.Value 440 } 441 } 442 return "" 443 } 444 445 // User return the current login user. 446 func (ctx *Context) User() interface{} { 447 return ctx.UserValue["user"] 448 } 449 450 // ServeContent serves content, headers are autoset 451 // receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string) 452 // 453 // You can define your own "Content-Type" header also, after this function call 454 // Doesn't implements resuming (by range), use ctx.SendFile instead 455 func (ctx *Context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error { 456 if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil { 457 ctx.WriteNotModified() 458 return nil 459 } 460 461 if ctx.GetContentType() == "" { 462 ctx.SetContentType(filename) 463 } 464 465 buf, _ := ioutil.ReadAll(content) 466 ctx.Response.Body = ioutil.NopCloser(bytes.NewBuffer(buf)) 467 return nil 468 } 469 470 // ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename) 471 func (ctx *Context) ServeFile(filename string, gzipCompression bool) error { 472 f, err := os.Open(filename) 473 if err != nil { 474 return fmt.Errorf("%d", http.StatusNotFound) 475 } 476 defer func() { 477 _ = f.Close() 478 }() 479 fi, _ := f.Stat() 480 if fi.IsDir() { 481 return ctx.ServeFile(path.Join(filename, "index.html"), gzipCompression) 482 } 483 484 return ctx.ServeContent(f, fi.Name(), fi.ModTime(), gzipCompression) 485 } 486 487 type HandlerMap map[Path]Handlers 488 489 // App is the key struct of the package. App as a member of plugin 490 // entity contains the request and the corresponding handler. Prefix 491 // is the url prefix and MiddlewareList is for control flow. 492 type App struct { 493 Requests []Path 494 Handlers HandlerMap 495 Middlewares Handlers 496 Prefix string 497 498 Routers RouterMap 499 routeIndex int 500 routeANY bool 501 } 502 503 // NewApp return an empty app. 504 func NewApp() *App { 505 return &App{ 506 Requests: make([]Path, 0), 507 Handlers: make(HandlerMap), 508 Prefix: "/", 509 Middlewares: make([]Handler, 0), 510 routeIndex: -1, 511 Routers: make(RouterMap), 512 } 513 } 514 515 // Handler defines the handler used by the middleware as return value. 516 type Handler func(ctx *Context) 517 518 // Handlers is the array of Handler 519 type Handlers []Handler 520 521 // AppendReqAndResp stores the request info and handle into app. 522 // support the route parameter. The route parameter will be recognized as 523 // wildcard store into the RegUrl of Path struct. For example: 524 // 525 // /user/:id => /user/(.*) 526 // /user/:id/info => /user/(.*?)/info 527 // 528 // The RegUrl will be used to recognize the incoming path and find 529 // the handler. 530 func (app *App) AppendReqAndResp(url, method string, handler []Handler) { 531 532 app.Requests = append(app.Requests, Path{ 533 URL: join(app.Prefix, url), 534 Method: method, 535 }) 536 app.routeIndex++ 537 538 app.Handlers[Path{ 539 URL: join(app.Prefix, url), 540 Method: method, 541 }] = append(app.Middlewares, handler...) 542 } 543 544 // Find is public helper method for findPath of tree. 545 func (app *App) Find(url, method string) []Handler { 546 app.routeANY = false 547 return app.Handlers[Path{URL: url, Method: method}] 548 } 549 550 // POST is a shortcut for app.AppendReqAndResp(url, "post", handler). 551 func (app *App) POST(url string, handler ...Handler) *App { 552 app.routeANY = false 553 app.AppendReqAndResp(url, "post", handler) 554 return app 555 } 556 557 // GET is a shortcut for app.AppendReqAndResp(url, "get", handler). 558 func (app *App) GET(url string, handler ...Handler) *App { 559 app.routeANY = false 560 app.AppendReqAndResp(url, "get", handler) 561 return app 562 } 563 564 // DELETE is a shortcut for app.AppendReqAndResp(url, "delete", handler). 565 func (app *App) DELETE(url string, handler ...Handler) *App { 566 app.routeANY = false 567 app.AppendReqAndResp(url, "delete", handler) 568 return app 569 } 570 571 // PUT is a shortcut for app.AppendReqAndResp(url, "put", handler). 572 func (app *App) PUT(url string, handler ...Handler) *App { 573 app.routeANY = false 574 app.AppendReqAndResp(url, "put", handler) 575 return app 576 } 577 578 // OPTIONS is a shortcut for app.AppendReqAndResp(url, "options", handler). 579 func (app *App) OPTIONS(url string, handler ...Handler) *App { 580 app.routeANY = false 581 app.AppendReqAndResp(url, "options", handler) 582 return app 583 } 584 585 // HEAD is a shortcut for app.AppendReqAndResp(url, "head", handler). 586 func (app *App) HEAD(url string, handler ...Handler) *App { 587 app.routeANY = false 588 app.AppendReqAndResp(url, "head", handler) 589 return app 590 } 591 592 // ANY registers a route that matches all the HTTP methods. 593 // GET, POST, PUT, HEAD, OPTIONS, DELETE. 594 func (app *App) ANY(url string, handler ...Handler) *App { 595 app.routeANY = true 596 app.AppendReqAndResp(url, "post", handler) 597 app.AppendReqAndResp(url, "get", handler) 598 app.AppendReqAndResp(url, "delete", handler) 599 app.AppendReqAndResp(url, "put", handler) 600 app.AppendReqAndResp(url, "options", handler) 601 app.AppendReqAndResp(url, "head", handler) 602 return app 603 } 604 605 func (app *App) Name(name string) { 606 if app.routeANY { 607 app.Routers[name] = Router{ 608 Methods: []string{"POST", "GET", "DELETE", "PUT", "OPTIONS", "HEAD"}, 609 Patten: app.Requests[app.routeIndex].URL, 610 } 611 } else { 612 app.Routers[name] = Router{ 613 Methods: []string{app.Requests[app.routeIndex].Method}, 614 Patten: app.Requests[app.routeIndex].URL, 615 } 616 } 617 } 618 619 // Group add middlewares and prefix for App. 620 func (app *App) Group(prefix string, middleware ...Handler) *RouterGroup { 621 return &RouterGroup{ 622 app: app, 623 Middlewares: append(app.Middlewares, middleware...), 624 Prefix: slash(prefix), 625 } 626 } 627 628 // RouterGroup is a group of routes. 629 type RouterGroup struct { 630 app *App 631 Middlewares Handlers 632 Prefix string 633 } 634 635 // AppendReqAndResp stores the request info and handle into app. 636 // support the route parameter. The route parameter will be recognized as 637 // wildcard store into the RegUrl of Path struct. For example: 638 // 639 // /user/:id => /user/(.*) 640 // /user/:id/info => /user/(.*?)/info 641 // 642 // The RegUrl will be used to recognize the incoming path and find 643 // the handler. 644 func (g *RouterGroup) AppendReqAndResp(url, method string, handler []Handler) { 645 646 g.app.Requests = append(g.app.Requests, Path{ 647 URL: join(g.Prefix, url), 648 Method: method, 649 }) 650 g.app.routeIndex++ 651 652 var h = make([]Handler, len(g.Middlewares)) 653 copy(h, g.Middlewares) 654 655 g.app.Handlers[Path{ 656 URL: join(g.Prefix, url), 657 Method: method, 658 }] = append(h, handler...) 659 } 660 661 // POST is a shortcut for app.AppendReqAndResp(url, "post", handler). 662 func (g *RouterGroup) POST(url string, handler ...Handler) *RouterGroup { 663 g.app.routeANY = false 664 g.AppendReqAndResp(url, "post", handler) 665 return g 666 } 667 668 // GET is a shortcut for app.AppendReqAndResp(url, "get", handler). 669 func (g *RouterGroup) GET(url string, handler ...Handler) *RouterGroup { 670 g.app.routeANY = false 671 g.AppendReqAndResp(url, "get", handler) 672 return g 673 } 674 675 // DELETE is a shortcut for app.AppendReqAndResp(url, "delete", handler). 676 func (g *RouterGroup) DELETE(url string, handler ...Handler) *RouterGroup { 677 g.app.routeANY = false 678 g.AppendReqAndResp(url, "delete", handler) 679 return g 680 } 681 682 // PUT is a shortcut for app.AppendReqAndResp(url, "put", handler). 683 func (g *RouterGroup) PUT(url string, handler ...Handler) *RouterGroup { 684 g.app.routeANY = false 685 g.AppendReqAndResp(url, "put", handler) 686 return g 687 } 688 689 // OPTIONS is a shortcut for app.AppendReqAndResp(url, "options", handler). 690 func (g *RouterGroup) OPTIONS(url string, handler ...Handler) *RouterGroup { 691 g.app.routeANY = false 692 g.AppendReqAndResp(url, "options", handler) 693 return g 694 } 695 696 // HEAD is a shortcut for app.AppendReqAndResp(url, "head", handler). 697 func (g *RouterGroup) HEAD(url string, handler ...Handler) *RouterGroup { 698 g.app.routeANY = false 699 g.AppendReqAndResp(url, "head", handler) 700 return g 701 } 702 703 // ANY registers a route that matches all the HTTP methods. 704 // GET, POST, PUT, HEAD, OPTIONS, DELETE. 705 func (g *RouterGroup) ANY(url string, handler ...Handler) *RouterGroup { 706 g.app.routeANY = true 707 g.AppendReqAndResp(url, "post", handler) 708 g.AppendReqAndResp(url, "get", handler) 709 g.AppendReqAndResp(url, "delete", handler) 710 g.AppendReqAndResp(url, "put", handler) 711 g.AppendReqAndResp(url, "options", handler) 712 g.AppendReqAndResp(url, "head", handler) 713 return g 714 } 715 716 func (g *RouterGroup) Name(name string) { 717 g.app.Name(name) 718 } 719 720 // Group add middlewares and prefix for RouterGroup. 721 func (g *RouterGroup) Group(prefix string, middleware ...Handler) *RouterGroup { 722 return &RouterGroup{ 723 app: g.app, 724 Middlewares: append(g.Middlewares, middleware...), 725 Prefix: join(slash(g.Prefix), slash(prefix)), 726 } 727 } 728 729 // slash fix the path which has wrong format problem. 730 // 731 // "" => "/" 732 // "abc/" => "/abc" 733 // "/abc/" => "/abc" 734 // "/abc" => "/abc" 735 // "/" => "/" 736 func slash(prefix string) string { 737 prefix = strings.TrimSpace(prefix) 738 if prefix == "" || prefix == "/" { 739 return "/" 740 } 741 if prefix[0] != '/' { 742 if prefix[len(prefix)-1] == '/' { 743 return "/" + prefix[:len(prefix)-1] 744 } 745 return "/" + prefix 746 } 747 if prefix[len(prefix)-1] == '/' { 748 return prefix[:len(prefix)-1] 749 } 750 return prefix 751 } 752 753 // join join the path. 754 func join(prefix, suffix string) string { 755 if prefix == "/" { 756 return suffix 757 } 758 if suffix == "/" { 759 return prefix 760 } 761 return prefix + suffix 762 }