Skip to content

Commit 17655cd

Browse files
thehowllafriks
authored andcommitted
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
1 parent 77f8bad commit 17655cd

File tree

5 files changed

+155
-34
lines changed

5 files changed

+155
-34
lines changed

Diff for: models/user.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func (u *User) BeforeUpdate() {
145145
if len(u.AvatarEmail) == 0 {
146146
u.AvatarEmail = u.Email
147147
}
148-
if len(u.AvatarEmail) > 0 {
148+
if len(u.AvatarEmail) > 0 && u.Avatar == "" {
149149
u.Avatar = base.HashEmail(u.AvatarEmail)
150150
}
151151
}

Diff for: modules/public/dynamic.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,5 @@ import (
1212

1313
// Static implements the macaron static handler for serving assets.
1414
func Static(opts *Options) macaron.Handler {
15-
return macaron.Static(
16-
opts.Directory,
17-
macaron.StaticOptions{
18-
SkipLogging: opts.SkipLogging,
19-
},
20-
)
15+
return opts.staticHandler(opts.Directory)
2116
}

Diff for: modules/public/public.go

+132-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
package public
66

77
import (
8+
"encoding/base64"
9+
"log"
10+
"net/http"
811
"path"
12+
"path/filepath"
13+
"strings"
14+
"time"
915

1016
"code.gitea.io/gitea/modules/setting"
1117
"gopkg.in/macaron.v1"
@@ -19,15 +25,135 @@ import (
1925
// Options represents the available options to configure the macaron handler.
2026
type Options struct {
2127
Directory string
28+
IndexFile string
2229
SkipLogging bool
30+
// if set to true, will enable caching. Expires header will also be set to
31+
// expire after the defined time.
32+
ExpiresAfter time.Duration
33+
FileSystem http.FileSystem
34+
Prefix string
2335
}
2436

2537
// Custom implements the macaron static handler for serving custom assets.
2638
func Custom(opts *Options) macaron.Handler {
27-
return macaron.Static(
28-
path.Join(setting.CustomPath, "public"),
29-
macaron.StaticOptions{
30-
SkipLogging: opts.SkipLogging,
31-
},
32-
)
39+
return opts.staticHandler(path.Join(setting.CustomPath, "public"))
40+
}
41+
42+
// staticFileSystem implements http.FileSystem interface.
43+
type staticFileSystem struct {
44+
dir *http.Dir
45+
}
46+
47+
func newStaticFileSystem(directory string) staticFileSystem {
48+
if !filepath.IsAbs(directory) {
49+
directory = filepath.Join(macaron.Root, directory)
50+
}
51+
dir := http.Dir(directory)
52+
return staticFileSystem{&dir}
53+
}
54+
55+
func (fs staticFileSystem) Open(name string) (http.File, error) {
56+
return fs.dir.Open(name)
57+
}
58+
59+
// StaticHandler sets up a new middleware for serving static files in the
60+
func StaticHandler(dir string, opts *Options) macaron.Handler {
61+
return opts.staticHandler(dir)
62+
}
63+
64+
func (opts *Options) staticHandler(dir string) macaron.Handler {
65+
// Defaults
66+
if len(opts.IndexFile) == 0 {
67+
opts.IndexFile = "index.html"
68+
}
69+
// Normalize the prefix if provided
70+
if opts.Prefix != "" {
71+
// Ensure we have a leading '/'
72+
if opts.Prefix[0] != '/' {
73+
opts.Prefix = "/" + opts.Prefix
74+
}
75+
// Remove any trailing '/'
76+
opts.Prefix = strings.TrimRight(opts.Prefix, "/")
77+
}
78+
if opts.FileSystem == nil {
79+
opts.FileSystem = newStaticFileSystem(dir)
80+
}
81+
82+
return func(ctx *macaron.Context, log *log.Logger) {
83+
opts.handle(ctx, log, opts)
84+
}
85+
}
86+
87+
func (opts *Options) handle(ctx *macaron.Context, log *log.Logger, opt *Options) bool {
88+
if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" {
89+
return false
90+
}
91+
92+
file := ctx.Req.URL.Path
93+
// if we have a prefix, filter requests by stripping the prefix
94+
if opt.Prefix != "" {
95+
if !strings.HasPrefix(file, opt.Prefix) {
96+
return false
97+
}
98+
file = file[len(opt.Prefix):]
99+
if file != "" && file[0] != '/' {
100+
return false
101+
}
102+
}
103+
104+
f, err := opt.FileSystem.Open(file)
105+
if err != nil {
106+
return false
107+
}
108+
defer f.Close()
109+
110+
fi, err := f.Stat()
111+
if err != nil {
112+
log.Printf("[Static] %q exists, but fails to open: %v", file, err)
113+
return true
114+
}
115+
116+
// Try to serve index file
117+
if fi.IsDir() {
118+
// Redirect if missing trailing slash.
119+
if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
120+
http.Redirect(ctx.Resp, ctx.Req.Request, ctx.Req.URL.Path+"/", http.StatusFound)
121+
return true
122+
}
123+
124+
f, err = opt.FileSystem.Open(file)
125+
if err != nil {
126+
return false // Discard error.
127+
}
128+
defer f.Close()
129+
130+
fi, err = f.Stat()
131+
if err != nil || fi.IsDir() {
132+
return true
133+
}
134+
}
135+
136+
if !opt.SkipLogging {
137+
log.Println("[Static] Serving " + file)
138+
}
139+
140+
// Add an Expires header to the static content
141+
if opt.ExpiresAfter > 0 {
142+
ctx.Resp.Header().Set("Expires", time.Now().Add(opt.ExpiresAfter).UTC().Format(http.TimeFormat))
143+
tag := GenerateETag(string(fi.Size()), fi.Name(), fi.ModTime().UTC().Format(http.TimeFormat))
144+
ctx.Resp.Header().Set("ETag", tag)
145+
if ctx.Req.Header.Get("If-None-Match") == tag {
146+
ctx.Resp.WriteHeader(304)
147+
return false
148+
}
149+
}
150+
151+
http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f)
152+
return true
153+
}
154+
155+
// GenerateETag generates an ETag based on size, filename and file modification time
156+
func GenerateETag(fileSize, fileName, modTime string) string {
157+
etag := fileSize + fileName + modTime
158+
return base64.StdEncoding.EncodeToString([]byte(etag))
33159
}

Diff for: modules/public/static.go

+10-13
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,14 @@ import (
1313

1414
// Static implements the macaron static handler for serving assets.
1515
func Static(opts *Options) macaron.Handler {
16-
return macaron.Static(
17-
opts.Directory,
18-
macaron.StaticOptions{
19-
SkipLogging: opts.SkipLogging,
20-
FileSystem: bindata.Static(bindata.Options{
21-
Asset: Asset,
22-
AssetDir: AssetDir,
23-
AssetInfo: AssetInfo,
24-
AssetNames: AssetNames,
25-
Prefix: "",
26-
}),
27-
},
28-
)
16+
opts.FileSystem = bindata.Static(bindata.Options{
17+
Asset: Asset,
18+
AssetDir: AssetDir,
19+
AssetInfo: AssetInfo,
20+
AssetNames: AssetNames,
21+
Prefix: "",
22+
})
23+
// we don't need to pass the directory, because the directory var is only
24+
// used when in the options there is no FileSystem.
25+
return opts.staticHandler("")
2926
}

Diff for: routers/routes/routes.go

+11-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package routes
77
import (
88
"os"
99
"path"
10+
"time"
1011

1112
"code.gitea.io/gitea/models"
1213
"code.gitea.io/gitea/modules/auth"
@@ -53,21 +54,23 @@ func NewMacaron() *macaron.Macaron {
5354
}
5455
m.Use(public.Custom(
5556
&public.Options{
56-
SkipLogging: setting.DisableRouterLog,
57+
SkipLogging: setting.DisableRouterLog,
58+
ExpiresAfter: time.Hour * 6,
5759
},
5860
))
5961
m.Use(public.Static(
6062
&public.Options{
61-
Directory: path.Join(setting.StaticRootPath, "public"),
62-
SkipLogging: setting.DisableRouterLog,
63+
Directory: path.Join(setting.StaticRootPath, "public"),
64+
SkipLogging: setting.DisableRouterLog,
65+
ExpiresAfter: time.Hour * 6,
6366
},
6467
))
65-
m.Use(macaron.Static(
68+
m.Use(public.StaticHandler(
6669
setting.AvatarUploadPath,
67-
macaron.StaticOptions{
68-
Prefix: "avatars",
69-
SkipLogging: setting.DisableRouterLog,
70-
ETag: true,
70+
&public.Options{
71+
Prefix: "avatars",
72+
SkipLogging: setting.DisableRouterLog,
73+
ExpiresAfter: time.Hour * 6,
7174
},
7275
))
7376

0 commit comments

Comments
 (0)