Skip to content
46 changes: 37 additions & 9 deletions modules/httpcache/httpcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"os"
"strconv"
"strings"
"time"

"code.gitea.io/gitea/modules/setting"
Expand All @@ -26,11 +27,13 @@ func GetCacheControl() string {
// 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))
return `"` + base64.StdEncoding.EncodeToString([]byte(etag)) + `"`
Comment thread
KN4CK3R marked this conversation as resolved.
}

// HandleTimeCache handles time-based caching for a HTTP request
func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) {
w.Header().Set("Cache-Control", GetCacheControl())

ifModifiedSince := req.Header.Get("If-Modified-Since")
if ifModifiedSince != "" {
t, err := time.Parse(http.TimeFormat, ifModifiedSince)
Expand All @@ -40,20 +43,45 @@ func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (
}
}

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) {
// HandleFileEtagCache handles ETag-based caching for a HTTP request
func HandleFileEtagCache(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
}
return HandleGenericETagCache(req, w, etag)
}

// HandleGenericETagCache handles ETag-based caching for a HTTP request.
// It returns true if the request was handled.
func HandleGenericETagCache(req *http.Request, w http.ResponseWriter, etag string) (handled bool) {
if len(etag) > 0 {
w.Header().Set("Etag", etag)
Comment thread
KN4CK3R marked this conversation as resolved.
if checkIfNoneMatchIsValid(req, etag) {
w.WriteHeader(http.StatusNotModified)
return true
}
}
w.Header().Set("Cache-Control", GetCacheControl())
w.Header().Set("ETag", etag)
return false
}

// checkIfNoneMatchIsValid tests if the header If-None-Match matches the ETag
func checkIfNoneMatchIsValid(req *http.Request, etag string) bool {
ifNoneMatch := req.Header.Get("If-None-Match")
if len(ifNoneMatch) > 0 {
if ifNoneMatch == "*" {
return true
}
Comment thread
KN4CK3R marked this conversation as resolved.
Outdated

etag = strings.TrimPrefix(etag, "W/")
for _, item := range strings.Split(ifNoneMatch, ",") {
item = strings.TrimPrefix(strings.TrimSpace(item), "W/")
Comment thread
KN4CK3R marked this conversation as resolved.
Outdated
if item == etag {
return true
}
}
}
return false
}
2 changes: 1 addition & 1 deletion modules/public/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ func (opts *Options) handle(w http.ResponseWriter, req *http.Request, opt *Optio
log.Println("[Static] Serving " + file)
}

if httpcache.HandleEtagCache(req, w, fi) {
if httpcache.HandleFileEtagCache(req, w, fi) {
return true
}

Expand Down
20 changes: 10 additions & 10 deletions routers/repo/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
Expand Down Expand Up @@ -124,21 +125,25 @@ func GetAttachment(ctx *context.Context) {
}
}

if err := attach.IncreaseDownloadCount(); err != nil {
Comment thread
KN4CK3R marked this conversation as resolved.
ctx.ServerError("IncreaseDownloadCount", err)
return
}

if setting.Attachment.ServeDirect {
//If we have a signed url (S3, object storage), redirect to this directly.
u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name)

if u != nil && err == nil {
if err := attach.IncreaseDownloadCount(); err != nil {
ctx.ServerError("Update", err)
return
}

ctx.Redirect(u.String())
return
}
}

if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+attach.UUID+`"`) {
return
}

//If we have matched and access to release or issue
fr, err := storage.Attachments.Open(attach.RelativePath())
if err != nil {
Expand All @@ -147,11 +152,6 @@ func GetAttachment(ctx *context.Context) {
}
defer fr.Close()

if err := attach.IncreaseDownloadCount(); err != nil {
ctx.ServerError("Update", err)
return
}

if err = ServeData(ctx, attach.Name, attach.Size, fr); err != nil {
ctx.ServerError("ServeData", err)
return
Expand Down
13 changes: 13 additions & 0 deletions routers/repo/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
)
Expand All @@ -31,6 +32,7 @@ func ServeData(ctx *context.Context, name string, size int64, reader io.Reader)
}

ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400")

if size >= 0 {
ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
} else {
Expand Down Expand Up @@ -71,6 +73,10 @@ func ServeData(ctx *context.Context, name string, size int64, reader io.Reader)

// ServeBlob download a git.Blob
func ServeBlob(ctx *context.Context, blob *git.Blob) error {
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`) {
return nil
}

dataRc, err := blob.DataAsync()
if err != nil {
return err
Expand All @@ -86,6 +92,10 @@ func ServeBlob(ctx *context.Context, blob *git.Blob) error {

// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error {
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`) {
return nil
}

dataRc, err := blob.DataAsync()
if err != nil {
return err
Expand All @@ -102,6 +112,9 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error {
if meta == nil {
return ServeBlob(ctx, blob)
}
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
return nil
}
lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer)
if err != nil {
return err
Expand Down