Skip to content

Commit 0615b66

Browse files
authored
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.
1 parent 9ec5e6c commit 0615b66

File tree

8 files changed

+91
-36
lines changed

8 files changed

+91
-36
lines changed

custom/conf/app.example.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ GRACEFUL_HAMMER_TIME = 60s
389389
; Allows the setting of a startup timeout and waithint for Windows as SVC service
390390
; 0 disables this.
391391
STARTUP_TIMEOUT = 0
392-
; Static resources, includes resources on custom/, public/ and all uploaded avatars web browser cache time, default is 6h
392+
; 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
393393
STATIC_CACHE_TIME = 6h
394394

395395
; Define allowed algorithms and their minimum key length (use -1 to disable a type)

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
262262
- `KEY_FILE`: **https/key.pem**: Key file path used for HTTPS. From 1.11 paths are relative to `CUSTOM_PATH`.
263263
- `STATIC_ROOT_PATH`: **./**: Upper level of template and static files path.
264264
- `APP_DATA_PATH`: **data** (**/data/gitea** on docker): Default path for application data.
265-
- `STATIC_CACHE_TIME`: **6h**: Web browser cache time for static resources on `custom/`, `public/` and all uploaded avatars.
265+
- `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".
266266
- `ENABLE_GZIP`: **false**: Enables application-level GZIP support.
267267
- `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>`
268268
- `PPROF_DATA_PATH`: **data/tmp/pprof**: `PPROF_DATA_PATH`, use an absolute path when you start gitea as service

main.go

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"os"
1212
"runtime"
1313
"strings"
14+
"time"
1415

1516
"code.gitea.io/gitea/cmd"
1617
"code.gitea.io/gitea/modules/log"
@@ -40,6 +41,7 @@ var (
4041
func init() {
4142
setting.AppVer = Version
4243
setting.AppBuiltWith = formatBuiltWith()
44+
setting.AppStartTime = time.Now().UTC()
4345

4446
// Grab the original help templates
4547
originalAppHelpTemplate = cli.AppHelpTemplate

modules/httpcache/httpcache.go

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2020 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package httpcache
6+
7+
import (
8+
"encoding/base64"
9+
"fmt"
10+
"net/http"
11+
"os"
12+
"strconv"
13+
"time"
14+
15+
"code.gitea.io/gitea/modules/setting"
16+
)
17+
18+
// GetCacheControl returns a suitable "Cache-Control" header value
19+
func GetCacheControl() string {
20+
if setting.RunMode == "dev" {
21+
return "no-store"
22+
}
23+
return "private, max-age=" + strconv.FormatInt(int64(setting.StaticCacheTime.Seconds()), 10)
24+
}
25+
26+
// generateETag generates an ETag based on size, filename and file modification time
27+
func generateETag(fi os.FileInfo) string {
28+
etag := fmt.Sprint(fi.Size()) + fi.Name() + fi.ModTime().UTC().Format(http.TimeFormat)
29+
return base64.StdEncoding.EncodeToString([]byte(etag))
30+
}
31+
32+
// HandleTimeCache handles time-based caching for a HTTP request
33+
func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) {
34+
ifModifiedSince := req.Header.Get("If-Modified-Since")
35+
if ifModifiedSince != "" {
36+
t, err := time.Parse(http.TimeFormat, ifModifiedSince)
37+
if err == nil && fi.ModTime().Unix() <= t.Unix() {
38+
w.WriteHeader(http.StatusNotModified)
39+
return true
40+
}
41+
}
42+
43+
w.Header().Set("Cache-Control", GetCacheControl())
44+
w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
45+
return false
46+
}
47+
48+
// HandleEtagCache handles ETag-based caching for a HTTP request
49+
func HandleEtagCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) {
50+
etag := generateETag(fi)
51+
if req.Header.Get("If-None-Match") == etag {
52+
w.WriteHeader(http.StatusNotModified)
53+
return true
54+
}
55+
56+
w.Header().Set("Cache-Control", GetCacheControl())
57+
w.Header().Set("ETag", etag)
58+
return false
59+
}

modules/public/public.go

+5-23
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@
55
package public
66

77
import (
8-
"encoding/base64"
9-
"fmt"
108
"log"
119
"net/http"
1210
"path"
1311
"path/filepath"
1412
"strings"
15-
"time"
1613

14+
"code.gitea.io/gitea/modules/httpcache"
1715
"code.gitea.io/gitea/modules/setting"
1816
)
1917

@@ -22,11 +20,8 @@ type Options struct {
2220
Directory string
2321
IndexFile string
2422
SkipLogging bool
25-
// if set to true, will enable caching. Expires header will also be set to
26-
// expire after the defined time.
27-
ExpiresAfter time.Duration
28-
FileSystem http.FileSystem
29-
Prefix string
23+
FileSystem http.FileSystem
24+
Prefix string
3025
}
3126

3227
// KnownPublicEntries list all direct children in the `public` directory
@@ -158,23 +153,10 @@ func (opts *Options) handle(w http.ResponseWriter, req *http.Request, opt *Optio
158153
log.Println("[Static] Serving " + file)
159154
}
160155

161-
// Add an Expires header to the static content
162-
if opt.ExpiresAfter > 0 {
163-
w.Header().Set("Expires", time.Now().Add(opt.ExpiresAfter).UTC().Format(http.TimeFormat))
164-
tag := GenerateETag(fmt.Sprint(fi.Size()), fi.Name(), fi.ModTime().UTC().Format(http.TimeFormat))
165-
w.Header().Set("ETag", tag)
166-
if req.Header.Get("If-None-Match") == tag {
167-
w.WriteHeader(304)
168-
return true
169-
}
156+
if httpcache.HandleEtagCache(req, w, fi) {
157+
return true
170158
}
171159

172160
http.ServeContent(w, req, file, fi.ModTime(), f)
173161
return true
174162
}
175-
176-
// GenerateETag generates an ETag based on size, filename and file modification time
177-
func GenerateETag(fileSize, fileName, modTime string) string {
178-
etag := fileSize + fileName + modTime
179-
return base64.StdEncoding.EncodeToString([]byte(etag))
180-
}

modules/setting/setting.go

+3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ var (
6767
// AppVer settings
6868
AppVer string
6969
AppBuiltWith string
70+
AppStartTime time.Time
7071
AppName string
7172
AppURL string
7273
AppSubURL string
@@ -362,6 +363,7 @@ var (
362363
PIDFile = "/run/gitea.pid"
363364
WritePIDFile bool
364365
ProdMode bool
366+
RunMode string
365367
RunUser string
366368
IsWindows bool
367369
HasRobotsTxt bool
@@ -837,6 +839,7 @@ func NewContext() {
837839
}
838840

839841
RunUser = Cfg.Section("").Key("RUN_USER").MustString(user.CurrentUsername())
842+
RunMode = Cfg.Section("").Key("RUN_MODE").MustString("dev")
840843
// Does not check run user when the install lock is off.
841844
if InstallLock {
842845
currentUser, match := IsRunUserMatchCurrentUser(RunUser)

routers/routes/chi.go

+16-11
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"text/template"
1717
"time"
1818

19+
"code.gitea.io/gitea/modules/httpcache"
1920
"code.gitea.io/gitea/modules/log"
2021
"code.gitea.io/gitea/modules/metrics"
2122
"code.gitea.io/gitea/modules/public"
@@ -162,6 +163,12 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor
162163

163164
rPath := strings.TrimPrefix(req.RequestURI, "/"+prefix)
164165
rPath = strings.TrimPrefix(rPath, "/")
166+
167+
fi, err := objStore.Stat(rPath)
168+
if err == nil && httpcache.HandleTimeCache(req, w, fi) {
169+
return
170+
}
171+
165172
//If we have matched and access to release or issue
166173
fr, err := objStore.Open(rPath)
167174
if err != nil {
@@ -200,21 +207,15 @@ func NewChi() chi.Router {
200207
setupAccessLogger(c)
201208
}
202209

203-
if setting.ProdMode {
204-
log.Warn("ProdMode ignored")
205-
}
206-
207210
c.Use(public.Custom(
208211
&public.Options{
209-
SkipLogging: setting.DisableRouterLog,
210-
ExpiresAfter: time.Hour * 6,
212+
SkipLogging: setting.DisableRouterLog,
211213
},
212214
))
213215
c.Use(public.Static(
214216
&public.Options{
215-
Directory: path.Join(setting.StaticRootPath, "public"),
216-
SkipLogging: setting.DisableRouterLog,
217-
ExpiresAfter: time.Hour * 6,
217+
Directory: path.Join(setting.StaticRootPath, "public"),
218+
SkipLogging: setting.DisableRouterLog,
218219
},
219220
))
220221

@@ -247,10 +248,14 @@ func NormalRoutes() http.Handler {
247248
w.WriteHeader(http.StatusOK)
248249
})
249250

250-
// robots.txt
251251
if setting.HasRobotsTxt {
252252
r.Get("/robots.txt", func(w http.ResponseWriter, req *http.Request) {
253-
http.ServeFile(w, req, path.Join(setting.CustomPath, "robots.txt"))
253+
filePath := path.Join(setting.CustomPath, "robots.txt")
254+
fi, err := os.Stat(filePath)
255+
if err == nil && httpcache.HandleTimeCache(req, w, fi) {
256+
return
257+
}
258+
http.ServeFile(w, req, filePath)
254259
})
255260
}
256261

routers/routes/macaron.go

+4
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ package routes
66

77
import (
88
"encoding/gob"
9+
"net/http"
910

1011
"code.gitea.io/gitea/models"
1112
"code.gitea.io/gitea/modules/auth"
1213
"code.gitea.io/gitea/modules/context"
14+
"code.gitea.io/gitea/modules/httpcache"
1315
"code.gitea.io/gitea/modules/lfs"
1416
"code.gitea.io/gitea/modules/log"
1517
"code.gitea.io/gitea/modules/options"
@@ -977,6 +979,8 @@ func RegisterMacaronRoutes(m *macaron.Macaron) {
977979

978980
// Progressive Web App
979981
m.Get("/manifest.json", templates.JSONRenderer(), func(ctx *context.Context) {
982+
ctx.Resp.Header().Set("Cache-Control", httpcache.GetCacheControl())
983+
ctx.Resp.Header().Set("Last-Modified", setting.AppStartTime.Format(http.TimeFormat))
980984
ctx.HTML(200, "pwa/manifest_json")
981985
})
982986

0 commit comments

Comments
 (0)