You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

public.go 3.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. // Copyright 2016 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package public
  5. import (
  6. "encoding/base64"
  7. "log"
  8. "net/http"
  9. "path"
  10. "path/filepath"
  11. "strings"
  12. "time"
  13. "code.gitea.io/gitea/modules/setting"
  14. "gitea.com/macaron/macaron"
  15. )
  16. //go:generate go run -mod=vendor main.go
  17. // Options represents the available options to configure the macaron handler.
  18. type Options struct {
  19. Directory string
  20. IndexFile string
  21. SkipLogging bool
  22. // if set to true, will enable caching. Expires header will also be set to
  23. // expire after the defined time.
  24. ExpiresAfter time.Duration
  25. FileSystem http.FileSystem
  26. Prefix string
  27. }
  28. // Custom implements the macaron static handler for serving custom assets.
  29. func Custom(opts *Options) macaron.Handler {
  30. return opts.staticHandler(path.Join(setting.CustomPath, "public"))
  31. }
  32. // staticFileSystem implements http.FileSystem interface.
  33. type staticFileSystem struct {
  34. dir *http.Dir
  35. }
  36. func newStaticFileSystem(directory string) staticFileSystem {
  37. if !filepath.IsAbs(directory) {
  38. directory = filepath.Join(macaron.Root, directory)
  39. }
  40. dir := http.Dir(directory)
  41. return staticFileSystem{&dir}
  42. }
  43. func (fs staticFileSystem) Open(name string) (http.File, error) {
  44. return fs.dir.Open(name)
  45. }
  46. // StaticHandler sets up a new middleware for serving static files in the
  47. func StaticHandler(dir string, opts *Options) macaron.Handler {
  48. return opts.staticHandler(dir)
  49. }
  50. func (opts *Options) staticHandler(dir string) macaron.Handler {
  51. // Defaults
  52. if len(opts.IndexFile) == 0 {
  53. opts.IndexFile = "index.html"
  54. }
  55. // Normalize the prefix if provided
  56. if opts.Prefix != "" {
  57. // Ensure we have a leading '/'
  58. if opts.Prefix[0] != '/' {
  59. opts.Prefix = "/" + opts.Prefix
  60. }
  61. // Remove any trailing '/'
  62. opts.Prefix = strings.TrimRight(opts.Prefix, "/")
  63. }
  64. if opts.FileSystem == nil {
  65. opts.FileSystem = newStaticFileSystem(dir)
  66. }
  67. return func(ctx *macaron.Context, log *log.Logger) {
  68. opts.handle(ctx, log, opts)
  69. }
  70. }
  71. func (opts *Options) handle(ctx *macaron.Context, log *log.Logger, opt *Options) bool {
  72. if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" {
  73. return false
  74. }
  75. file := ctx.Req.URL.Path
  76. // if we have a prefix, filter requests by stripping the prefix
  77. if opt.Prefix != "" {
  78. if !strings.HasPrefix(file, opt.Prefix) {
  79. return false
  80. }
  81. file = file[len(opt.Prefix):]
  82. if file != "" && file[0] != '/' {
  83. return false
  84. }
  85. }
  86. f, err := opt.FileSystem.Open(file)
  87. if err != nil {
  88. return false
  89. }
  90. defer f.Close()
  91. fi, err := f.Stat()
  92. if err != nil {
  93. log.Printf("[Static] %q exists, but fails to open: %v", file, err)
  94. return true
  95. }
  96. // Try to serve index file
  97. if fi.IsDir() {
  98. // Redirect if missing trailing slash.
  99. if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
  100. http.Redirect(ctx.Resp, ctx.Req.Request, path.Clean(ctx.Req.URL.Path+"/"), http.StatusFound)
  101. return true
  102. }
  103. f, err = opt.FileSystem.Open(file)
  104. if err != nil {
  105. return false // Discard error.
  106. }
  107. defer f.Close()
  108. fi, err = f.Stat()
  109. if err != nil || fi.IsDir() {
  110. return true
  111. }
  112. }
  113. if !opt.SkipLogging {
  114. log.Println("[Static] Serving " + file)
  115. }
  116. // Add an Expires header to the static content
  117. if opt.ExpiresAfter > 0 {
  118. ctx.Resp.Header().Set("Expires", time.Now().Add(opt.ExpiresAfter).UTC().Format(http.TimeFormat))
  119. tag := GenerateETag(string(fi.Size()), fi.Name(), fi.ModTime().UTC().Format(http.TimeFormat))
  120. ctx.Resp.Header().Set("ETag", tag)
  121. if ctx.Req.Header.Get("If-None-Match") == tag {
  122. ctx.Resp.WriteHeader(304)
  123. return false
  124. }
  125. }
  126. http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f)
  127. return true
  128. }
  129. // GenerateETag generates an ETag based on size, filename and file modification time
  130. func GenerateETag(fileSize, fileName, modTime string) string {
  131. etag := fileSize + fileName + modTime
  132. return base64.StdEncoding.EncodeToString([]byte(etag))
  133. }