Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable caching on assets and avatars #3376

Merged
merged 6 commits into from
Feb 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
7 changes: 1 addition & 6 deletions modules/public/dynamic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
138 changes: 132 additions & 6 deletions modules/public/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of staticHandler has been copied over from parts of macaron. The handle function was modified to use ExpireAfter instead of the Expires function, and also now it has proper support for ETags (by reading the req header If-None-Match).

// 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should at least log error in such case

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))
}
23 changes: 10 additions & 13 deletions modules/public/static.go
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
}
19 changes: 11 additions & 8 deletions routers/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package routes
import (
"os"
"path"
"time"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
Expand Down Expand Up @@ -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,
},
))

Expand Down