forked from gitea/gitea
		
	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{ | ||||
| 				Asset:      Asset, | ||||
| 				AssetDir:   AssetDir, | ||||
| 				AssetInfo:  AssetInfo, | ||||
| 				AssetNames: AssetNames, | ||||
| 				Prefix:     "", | ||||
| 			}), | ||||
| 		}, | ||||
| 	) | ||||
| 	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" | ||||
| @ -53,21 +54,23 @@ func NewMacaron() *macaron.Macaron { | ||||
| 	} | ||||
| 	m.Use(public.Custom( | ||||
| 		&public.Options{ | ||||
| 			SkipLogging: setting.DisableRouterLog, | ||||
| 			SkipLogging:  setting.DisableRouterLog, | ||||
| 			ExpiresAfter: time.Hour * 6, | ||||
| 		}, | ||||
| 	)) | ||||
| 	m.Use(public.Static( | ||||
| 		&public.Options{ | ||||
| 			Directory:   path.Join(setting.StaticRootPath, "public"), | ||||
| 			SkipLogging: setting.DisableRouterLog, | ||||
| 			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{ | ||||
| 			Prefix:      "avatars", | ||||
| 			SkipLogging: setting.DisableRouterLog, | ||||
| 			ETag:        true, | ||||
| 		&public.Options{ | ||||
| 			Prefix:       "avatars", | ||||
| 			SkipLogging:  setting.DisableRouterLog, | ||||
| 			ExpiresAfter: time.Hour * 6, | ||||
| 		}, | ||||
| 	)) | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Morgan Bazalgette
						Morgan Bazalgette