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
This commit is contained in:
		
							parent
							
								
									77f8bad2fb
								
							
						
					
					
						commit
						17655cdf1b
					
				| 
						 | 
				
			
			@ -145,7 +145,7 @@ func (u *User) BeforeUpdate() {
 | 
			
		|||
		if len(u.AvatarEmail) == 0 {
 | 
			
		||||
			u.AvatarEmail = u.Email
 | 
			
		||||
		}
 | 
			
		||||
		if len(u.AvatarEmail) > 0 {
 | 
			
		||||
		if len(u.AvatarEmail) > 0 && u.Avatar == "" {
 | 
			
		||||
			u.Avatar = base.HashEmail(u.AvatarEmail)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,10 +12,5 @@ import (
 | 
			
		|||
 | 
			
		||||
// Static implements the macaron static handler for serving assets.
 | 
			
		||||
func Static(opts *Options) macaron.Handler {
 | 
			
		||||
	return macaron.Static(
 | 
			
		||||
		opts.Directory,
 | 
			
		||||
		macaron.StaticOptions{
 | 
			
		||||
			SkipLogging: opts.SkipLogging,
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	return opts.staticHandler(opts.Directory)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,17 +13,14 @@ import (
 | 
			
		|||
 | 
			
		||||
// Static implements the macaron static handler for serving assets.
 | 
			
		||||
func Static(opts *Options) macaron.Handler {
 | 
			
		||||
	return macaron.Static(
 | 
			
		||||
		opts.Directory,
 | 
			
		||||
		macaron.StaticOptions{
 | 
			
		||||
			SkipLogging: opts.SkipLogging,
 | 
			
		||||
			FileSystem: bindata.Static(bindata.Options{
 | 
			
		||||
	opts.FileSystem = bindata.Static(bindata.Options{
 | 
			
		||||
		Asset:      Asset,
 | 
			
		||||
		AssetDir:   AssetDir,
 | 
			
		||||
		AssetInfo:  AssetInfo,
 | 
			
		||||
		AssetNames: AssetNames,
 | 
			
		||||
		Prefix:     "",
 | 
			
		||||
			}),
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	})
 | 
			
		||||
	// we don't need to pass the directory, because the directory var is only
 | 
			
		||||
	// used when in the options there is no FileSystem.
 | 
			
		||||
	return opts.staticHandler("")
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ package routes
 | 
			
		|||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/modules/auth"
 | 
			
		||||
| 
						 | 
				
			
			@ -54,20 +55,22 @@ func NewMacaron() *macaron.Macaron {
 | 
			
		|||
	m.Use(public.Custom(
 | 
			
		||||
		&public.Options{
 | 
			
		||||
			SkipLogging:  setting.DisableRouterLog,
 | 
			
		||||
			ExpiresAfter: time.Hour * 6,
 | 
			
		||||
		},
 | 
			
		||||
	))
 | 
			
		||||
	m.Use(public.Static(
 | 
			
		||||
		&public.Options{
 | 
			
		||||
			Directory:    path.Join(setting.StaticRootPath, "public"),
 | 
			
		||||
			SkipLogging:  setting.DisableRouterLog,
 | 
			
		||||
			ExpiresAfter: time.Hour * 6,
 | 
			
		||||
		},
 | 
			
		||||
	))
 | 
			
		||||
	m.Use(macaron.Static(
 | 
			
		||||
	m.Use(public.StaticHandler(
 | 
			
		||||
		setting.AvatarUploadPath,
 | 
			
		||||
		macaron.StaticOptions{
 | 
			
		||||
		&public.Options{
 | 
			
		||||
			Prefix:       "avatars",
 | 
			
		||||
			SkipLogging:  setting.DisableRouterLog,
 | 
			
		||||
			ETag:        true,
 | 
			
		||||
			ExpiresAfter: time.Hour * 6,
 | 
			
		||||
		},
 | 
			
		||||
	))
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue