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