summaryrefslogtreecommitdiffstats
path: root/modules/public/public.go
diff options
context:
space:
mode:
authorMorgan Bazalgette <git@howl.moe>2018-02-03 23:37:05 +0100
committerLauris BH <lauris@nix.lv>2018-02-04 00:37:05 +0200
commit17655cdf1b409521262d5d54eb19884d307c47ce (patch)
treeb06d15367afc6bd4f915c0e8b9272241bb3f4a3c /modules/public/public.go
parent77f8bad2fb7a6a4ab57b398cb89e6889f76ffe8a (diff)
downloadgitea-17655cdf1b409521262d5d54eb19884d307c47ce.tar.gz
gitea-17655cdf1b409521262d5d54eb19884d307c47ce.zip
Enable caching on assets and avatars (#3376)
* Enable caching on assets and avatars Fixes #3323 * Only set avatar in user BeforeUpdate when there is no avatar set * add error checking after stat * gofmt * Change cache time for avatars to an hour
Diffstat (limited to 'modules/public/public.go')
-rw-r--r--modules/public/public.go138
1 files changed, 132 insertions, 6 deletions
diff --git a/modules/public/public.go b/modules/public/public.go
index 6f28ebc032..f03f8fcc15 100644
--- a/modules/public/public.go
+++ b/modules/public/public.go
@@ -5,7 +5,13 @@
package public
import (
+ "encoding/base64"
+ "log"
+ "net/http"
"path"
+ "path/filepath"
+ "strings"
+ "time"
"code.gitea.io/gitea/modules/setting"
"gopkg.in/macaron.v1"
@@ -19,15 +25,135 @@ import (
// Options represents the available options to configure the macaron handler.
type Options struct {
Directory string
+ IndexFile string
SkipLogging bool
+ // if set to true, will enable caching. Expires header will also be set to
+ // expire after the defined time.
+ ExpiresAfter time.Duration
+ FileSystem http.FileSystem
+ Prefix string
}
// Custom implements the macaron static handler for serving custom assets.
func Custom(opts *Options) macaron.Handler {
- return macaron.Static(
- path.Join(setting.CustomPath, "public"),
- macaron.StaticOptions{
- SkipLogging: opts.SkipLogging,
- },
- )
+ return opts.staticHandler(path.Join(setting.CustomPath, "public"))
+}
+
+// staticFileSystem implements http.FileSystem interface.
+type staticFileSystem struct {
+ dir *http.Dir
+}
+
+func newStaticFileSystem(directory string) staticFileSystem {
+ if !filepath.IsAbs(directory) {
+ directory = filepath.Join(macaron.Root, directory)
+ }
+ dir := http.Dir(directory)
+ return staticFileSystem{&dir}
+}
+
+func (fs staticFileSystem) Open(name string) (http.File, error) {
+ return fs.dir.Open(name)
+}
+
+// StaticHandler sets up a new middleware for serving static files in the
+func StaticHandler(dir string, opts *Options) macaron.Handler {
+ return opts.staticHandler(dir)
+}
+
+func (opts *Options) staticHandler(dir string) macaron.Handler {
+ // Defaults
+ if len(opts.IndexFile) == 0 {
+ opts.IndexFile = "index.html"
+ }
+ // Normalize the prefix if provided
+ if opts.Prefix != "" {
+ // Ensure we have a leading '/'
+ if opts.Prefix[0] != '/' {
+ opts.Prefix = "/" + opts.Prefix
+ }
+ // Remove any trailing '/'
+ opts.Prefix = strings.TrimRight(opts.Prefix, "/")
+ }
+ if opts.FileSystem == nil {
+ opts.FileSystem = newStaticFileSystem(dir)
+ }
+
+ return func(ctx *macaron.Context, log *log.Logger) {
+ opts.handle(ctx, log, opts)
+ }
+}
+
+func (opts *Options) handle(ctx *macaron.Context, log *log.Logger, opt *Options) bool {
+ if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" {
+ return false
+ }
+
+ file := ctx.Req.URL.Path
+ // if we have a prefix, filter requests by stripping the prefix
+ if opt.Prefix != "" {
+ if !strings.HasPrefix(file, opt.Prefix) {
+ return false
+ }
+ file = file[len(opt.Prefix):]
+ if file != "" && file[0] != '/' {
+ return false
+ }
+ }
+
+ f, err := opt.FileSystem.Open(file)
+ if err != nil {
+ return false
+ }
+ defer f.Close()
+
+ fi, err := f.Stat()
+ if err != nil {
+ log.Printf("[Static] %q exists, but fails to open: %v", file, err)
+ return true
+ }
+
+ // Try to serve index file
+ if fi.IsDir() {
+ // Redirect if missing trailing slash.
+ if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
+ http.Redirect(ctx.Resp, ctx.Req.Request, ctx.Req.URL.Path+"/", http.StatusFound)
+ return true
+ }
+
+ f, err = opt.FileSystem.Open(file)
+ if err != nil {
+ return false // Discard error.
+ }
+ defer f.Close()
+
+ fi, err = f.Stat()
+ if err != nil || fi.IsDir() {
+ return true
+ }
+ }
+
+ if !opt.SkipLogging {
+ log.Println("[Static] Serving " + file)
+ }
+
+ // Add an Expires header to the static content
+ if opt.ExpiresAfter > 0 {
+ ctx.Resp.Header().Set("Expires", time.Now().Add(opt.ExpiresAfter).UTC().Format(http.TimeFormat))
+ tag := GenerateETag(string(fi.Size()), fi.Name(), fi.ModTime().UTC().Format(http.TimeFormat))
+ ctx.Resp.Header().Set("ETag", tag)
+ if ctx.Req.Header.Get("If-None-Match") == tag {
+ ctx.Resp.WriteHeader(304)
+ return false
+ }
+ }
+
+ http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f)
+ return true
+}
+
+// GenerateETag generates an ETag based on size, filename and file modification time
+func GenerateETag(fileSize, fileName, modTime string) string {
+ etag := fileSize + fileName + modTime
+ return base64.StdEncoding.EncodeToString([]byte(etag))
}