mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	HTTP cache rework and enable caching for storage assets (#13569)
This enabled HTTP time-based cache for storage assets, primarily avatars. I have not observed If-Modified-Since from browsers during tests but I guess it's good to support regardless. It introduces a new generic httpcache module that can handle both time-based and etag-based caching. Additionally, manifest.json and robots.txt are now also cachable.
This commit is contained in:
		| @@ -389,7 +389,7 @@ GRACEFUL_HAMMER_TIME = 60s | |||||||
| ; Allows the setting of a startup timeout and waithint for Windows as SVC service | ; Allows the setting of a startup timeout and waithint for Windows as SVC service | ||||||
| ; 0 disables this. | ; 0 disables this. | ||||||
| STARTUP_TIMEOUT = 0 | STARTUP_TIMEOUT = 0 | ||||||
| ; Static resources, includes resources on custom/, public/ and all uploaded avatars web browser cache time, default is 6h | ; Static resources, includes resources on custom/, public/ and all uploaded avatars web browser cache time. Note that this cache is disabled when RUN_MODE is "dev". Default is 6h | ||||||
| STATIC_CACHE_TIME = 6h | STATIC_CACHE_TIME = 6h | ||||||
|  |  | ||||||
| ; Define allowed algorithms and their minimum key length (use -1 to disable a type) | ; Define allowed algorithms and their minimum key length (use -1 to disable a type) | ||||||
|   | |||||||
| @@ -262,7 +262,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. | |||||||
| - `KEY_FILE`: **https/key.pem**: Key file path used for HTTPS. From 1.11 paths are relative to `CUSTOM_PATH`. | - `KEY_FILE`: **https/key.pem**: Key file path used for HTTPS. From 1.11 paths are relative to `CUSTOM_PATH`. | ||||||
| - `STATIC_ROOT_PATH`: **./**: Upper level of template and static files path. | - `STATIC_ROOT_PATH`: **./**: Upper level of template and static files path. | ||||||
| - `APP_DATA_PATH`: **data** (**/data/gitea** on docker): Default path for application data. | - `APP_DATA_PATH`: **data** (**/data/gitea** on docker): Default path for application data. | ||||||
| - `STATIC_CACHE_TIME`: **6h**: Web browser cache time for static resources on `custom/`, `public/` and all uploaded avatars. | - `STATIC_CACHE_TIME`: **6h**: Web browser cache time for static resources on `custom/`, `public/` and all uploaded avatars. Note that this cache is disabled when `RUN_MODE` is "dev". | ||||||
| - `ENABLE_GZIP`: **false**: Enables application-level GZIP support. | - `ENABLE_GZIP`: **false**: Enables application-level GZIP support. | ||||||
| - `ENABLE_PPROF`: **false**: Application profiling (memory and cpu). For "web" command it listens on localhost:6060. For "serv" command it dumps to disk at `PPROF_DATA_PATH` as `(cpuprofile|memprofile)_<username>_<temporary id>` | - `ENABLE_PPROF`: **false**: Application profiling (memory and cpu). For "web" command it listens on localhost:6060. For "serv" command it dumps to disk at `PPROF_DATA_PATH` as `(cpuprofile|memprofile)_<username>_<temporary id>` | ||||||
| - `PPROF_DATA_PATH`: **data/tmp/pprof**: `PPROF_DATA_PATH`, use an absolute path when you start gitea as service | - `PPROF_DATA_PATH`: **data/tmp/pprof**: `PPROF_DATA_PATH`, use an absolute path when you start gitea as service | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								main.go
									
									
									
									
									
								
							| @@ -11,6 +11,7 @@ import ( | |||||||
| 	"os" | 	"os" | ||||||
| 	"runtime" | 	"runtime" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/cmd" | 	"code.gitea.io/gitea/cmd" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| @@ -40,6 +41,7 @@ var ( | |||||||
| func init() { | func init() { | ||||||
| 	setting.AppVer = Version | 	setting.AppVer = Version | ||||||
| 	setting.AppBuiltWith = formatBuiltWith() | 	setting.AppBuiltWith = formatBuiltWith() | ||||||
|  | 	setting.AppStartTime = time.Now().UTC() | ||||||
|  |  | ||||||
| 	// Grab the original help templates | 	// Grab the original help templates | ||||||
| 	originalAppHelpTemplate = cli.AppHelpTemplate | 	originalAppHelpTemplate = cli.AppHelpTemplate | ||||||
|   | |||||||
							
								
								
									
										59
									
								
								modules/httpcache/httpcache.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								modules/httpcache/httpcache.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | // Copyright 2020 The Gitea Authors. All rights reserved. | ||||||
|  | // Use of this source code is governed by a MIT-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  |  | ||||||
|  | package httpcache | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  | 	"strconv" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // GetCacheControl returns a suitable "Cache-Control" header value | ||||||
|  | func GetCacheControl() string { | ||||||
|  | 	if setting.RunMode == "dev" { | ||||||
|  | 		return "no-store" | ||||||
|  | 	} | ||||||
|  | 	return "private, max-age=" + strconv.FormatInt(int64(setting.StaticCacheTime.Seconds()), 10) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // generateETag generates an ETag based on size, filename and file modification time | ||||||
|  | func generateETag(fi os.FileInfo) string { | ||||||
|  | 	etag := fmt.Sprint(fi.Size()) + fi.Name() + fi.ModTime().UTC().Format(http.TimeFormat) | ||||||
|  | 	return base64.StdEncoding.EncodeToString([]byte(etag)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // HandleTimeCache handles time-based caching for a HTTP request | ||||||
|  | func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) { | ||||||
|  | 	ifModifiedSince := req.Header.Get("If-Modified-Since") | ||||||
|  | 	if ifModifiedSince != "" { | ||||||
|  | 		t, err := time.Parse(http.TimeFormat, ifModifiedSince) | ||||||
|  | 		if err == nil && fi.ModTime().Unix() <= t.Unix() { | ||||||
|  | 			w.WriteHeader(http.StatusNotModified) | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	w.Header().Set("Cache-Control", GetCacheControl()) | ||||||
|  | 	w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat)) | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // HandleEtagCache handles ETag-based caching for a HTTP request | ||||||
|  | func HandleEtagCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) { | ||||||
|  | 	etag := generateETag(fi) | ||||||
|  | 	if req.Header.Get("If-None-Match") == etag { | ||||||
|  | 		w.WriteHeader(http.StatusNotModified) | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	w.Header().Set("Cache-Control", GetCacheControl()) | ||||||
|  | 	w.Header().Set("ETag", etag) | ||||||
|  | 	return false | ||||||
|  | } | ||||||
| @@ -5,15 +5,13 @@ | |||||||
| package public | package public | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/base64" |  | ||||||
| 	"fmt" |  | ||||||
| 	"log" | 	"log" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"path" | 	"path" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/httpcache" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -22,9 +20,6 @@ type Options struct { | |||||||
| 	Directory   string | 	Directory   string | ||||||
| 	IndexFile   string | 	IndexFile   string | ||||||
| 	SkipLogging bool | 	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 | 	FileSystem  http.FileSystem | ||||||
| 	Prefix      string | 	Prefix      string | ||||||
| } | } | ||||||
| @@ -158,23 +153,10 @@ func (opts *Options) handle(w http.ResponseWriter, req *http.Request, opt *Optio | |||||||
| 		log.Println("[Static] Serving " + file) | 		log.Println("[Static] Serving " + file) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Add an Expires header to the static content | 	if httpcache.HandleEtagCache(req, w, fi) { | ||||||
| 	if opt.ExpiresAfter > 0 { |  | ||||||
| 		w.Header().Set("Expires", time.Now().Add(opt.ExpiresAfter).UTC().Format(http.TimeFormat)) |  | ||||||
| 		tag := GenerateETag(fmt.Sprint(fi.Size()), fi.Name(), fi.ModTime().UTC().Format(http.TimeFormat)) |  | ||||||
| 		w.Header().Set("ETag", tag) |  | ||||||
| 		if req.Header.Get("If-None-Match") == tag { |  | ||||||
| 			w.WriteHeader(304) |  | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	http.ServeContent(w, req, file, fi.ModTime(), f) | 	http.ServeContent(w, req, file, fi.ModTime(), f) | ||||||
| 	return true | 	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)) |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -67,6 +67,7 @@ var ( | |||||||
| 	// AppVer settings | 	// AppVer settings | ||||||
| 	AppVer         string | 	AppVer         string | ||||||
| 	AppBuiltWith   string | 	AppBuiltWith   string | ||||||
|  | 	AppStartTime   time.Time | ||||||
| 	AppName        string | 	AppName        string | ||||||
| 	AppURL         string | 	AppURL         string | ||||||
| 	AppSubURL      string | 	AppSubURL      string | ||||||
| @@ -362,6 +363,7 @@ var ( | |||||||
| 	PIDFile       = "/run/gitea.pid" | 	PIDFile       = "/run/gitea.pid" | ||||||
| 	WritePIDFile  bool | 	WritePIDFile  bool | ||||||
| 	ProdMode      bool | 	ProdMode      bool | ||||||
|  | 	RunMode       string | ||||||
| 	RunUser       string | 	RunUser       string | ||||||
| 	IsWindows     bool | 	IsWindows     bool | ||||||
| 	HasRobotsTxt  bool | 	HasRobotsTxt  bool | ||||||
| @@ -837,6 +839,7 @@ func NewContext() { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	RunUser = Cfg.Section("").Key("RUN_USER").MustString(user.CurrentUsername()) | 	RunUser = Cfg.Section("").Key("RUN_USER").MustString(user.CurrentUsername()) | ||||||
|  | 	RunMode = Cfg.Section("").Key("RUN_MODE").MustString("dev") | ||||||
| 	// Does not check run user when the install lock is off. | 	// Does not check run user when the install lock is off. | ||||||
| 	if InstallLock { | 	if InstallLock { | ||||||
| 		currentUser, match := IsRunUserMatchCurrentUser(RunUser) | 		currentUser, match := IsRunUserMatchCurrentUser(RunUser) | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ import ( | |||||||
| 	"text/template" | 	"text/template" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/httpcache" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/metrics" | 	"code.gitea.io/gitea/modules/metrics" | ||||||
| 	"code.gitea.io/gitea/modules/public" | 	"code.gitea.io/gitea/modules/public" | ||||||
| @@ -162,6 +163,12 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor | |||||||
|  |  | ||||||
| 			rPath := strings.TrimPrefix(req.RequestURI, "/"+prefix) | 			rPath := strings.TrimPrefix(req.RequestURI, "/"+prefix) | ||||||
| 			rPath = strings.TrimPrefix(rPath, "/") | 			rPath = strings.TrimPrefix(rPath, "/") | ||||||
|  |  | ||||||
|  | 			fi, err := objStore.Stat(rPath) | ||||||
|  | 			if err == nil && httpcache.HandleTimeCache(req, w, fi) { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 			//If we have matched and access to release or issue | 			//If we have matched and access to release or issue | ||||||
| 			fr, err := objStore.Open(rPath) | 			fr, err := objStore.Open(rPath) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| @@ -200,21 +207,15 @@ func NewChi() chi.Router { | |||||||
| 		setupAccessLogger(c) | 		setupAccessLogger(c) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if setting.ProdMode { |  | ||||||
| 		log.Warn("ProdMode ignored") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	c.Use(public.Custom( | 	c.Use(public.Custom( | ||||||
| 		&public.Options{ | 		&public.Options{ | ||||||
| 			SkipLogging: setting.DisableRouterLog, | 			SkipLogging: setting.DisableRouterLog, | ||||||
| 			ExpiresAfter: time.Hour * 6, |  | ||||||
| 		}, | 		}, | ||||||
| 	)) | 	)) | ||||||
| 	c.Use(public.Static( | 	c.Use(public.Static( | ||||||
| 		&public.Options{ | 		&public.Options{ | ||||||
| 			Directory:   path.Join(setting.StaticRootPath, "public"), | 			Directory:   path.Join(setting.StaticRootPath, "public"), | ||||||
| 			SkipLogging: setting.DisableRouterLog, | 			SkipLogging: setting.DisableRouterLog, | ||||||
| 			ExpiresAfter: time.Hour * 6, |  | ||||||
| 		}, | 		}, | ||||||
| 	)) | 	)) | ||||||
|  |  | ||||||
| @@ -247,10 +248,14 @@ func NormalRoutes() http.Handler { | |||||||
| 		w.WriteHeader(http.StatusOK) | 		w.WriteHeader(http.StatusOK) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	// robots.txt |  | ||||||
| 	if setting.HasRobotsTxt { | 	if setting.HasRobotsTxt { | ||||||
| 		r.Get("/robots.txt", func(w http.ResponseWriter, req *http.Request) { | 		r.Get("/robots.txt", func(w http.ResponseWriter, req *http.Request) { | ||||||
| 			http.ServeFile(w, req, path.Join(setting.CustomPath, "robots.txt")) | 			filePath := path.Join(setting.CustomPath, "robots.txt") | ||||||
|  | 			fi, err := os.Stat(filePath) | ||||||
|  | 			if err == nil && httpcache.HandleTimeCache(req, w, fi) { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			http.ServeFile(w, req, filePath) | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,10 +6,12 @@ package routes | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/gob" | 	"encoding/gob" | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/modules/auth" | 	"code.gitea.io/gitea/modules/auth" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
|  | 	"code.gitea.io/gitea/modules/httpcache" | ||||||
| 	"code.gitea.io/gitea/modules/lfs" | 	"code.gitea.io/gitea/modules/lfs" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/options" | 	"code.gitea.io/gitea/modules/options" | ||||||
| @@ -977,6 +979,8 @@ func RegisterMacaronRoutes(m *macaron.Macaron) { | |||||||
|  |  | ||||||
| 	// Progressive Web App | 	// Progressive Web App | ||||||
| 	m.Get("/manifest.json", templates.JSONRenderer(), func(ctx *context.Context) { | 	m.Get("/manifest.json", templates.JSONRenderer(), func(ctx *context.Context) { | ||||||
|  | 		ctx.Resp.Header().Set("Cache-Control", httpcache.GetCacheControl()) | ||||||
|  | 		ctx.Resp.Header().Set("Last-Modified", setting.AppStartTime.Format(http.TimeFormat)) | ||||||
| 		ctx.HTML(200, "pwa/manifest_json") | 		ctx.HTML(200, "pwa/manifest_json") | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 silverwind
					silverwind