diff options
author | Morgan Bazalgette <git@howl.moe> | 2018-02-03 23:37:05 +0100 |
---|---|---|
committer | Lauris BH <lauris@nix.lv> | 2018-02-04 00:37:05 +0200 |
commit | 17655cdf1b409521262d5d54eb19884d307c47ce (patch) | |
tree | b06d15367afc6bd4f915c0e8b9272241bb3f4a3c /modules/public/public.go | |
parent | 77f8bad2fb7a6a4ab57b398cb89e6889f76ffe8a (diff) | |
download | gitea-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.go | 138 |
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)) } |