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.

159 lines
4.1 KiB

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