diff --git a/internal/api/routes_gallery.go b/internal/api/routes_gallery.go index e08663a70..585d46d4a 100644 --- a/internal/api/routes_gallery.go +++ b/internal/api/routes_gallery.go @@ -78,7 +78,7 @@ func (rs galleryRoutes) Cover(w http.ResponseWriter, r *http.Request) { return } - rs.imageRoutes.serveThumbnail(w, r, i) + rs.imageRoutes.serveThumbnail(w, r, i, &g.UpdatedAt) } func (rs galleryRoutes) Preview(w http.ResponseWriter, r *http.Request) { @@ -116,7 +116,7 @@ func (rs galleryRoutes) Preview(w http.ResponseWriter, r *http.Request) { return } - rs.imageRoutes.serveThumbnail(w, r, i) + rs.imageRoutes.serveThumbnail(w, r, i, nil) } func (rs galleryRoutes) GalleryCtx(next http.Handler) http.Handler { diff --git a/internal/api/routes_image.go b/internal/api/routes_image.go index 89e6d2db4..598a6fe26 100644 --- a/internal/api/routes_image.go +++ b/internal/api/routes_image.go @@ -7,6 +7,7 @@ import ( "net/http" "os/exec" "strconv" + "time" "github.com/go-chi/chi/v5" @@ -47,17 +48,21 @@ func (rs imageRoutes) Routes() chi.Router { func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) { img := r.Context().Value(imageKey).(*models.Image) - rs.serveThumbnail(w, r, img) + rs.serveThumbnail(w, r, img, nil) } -func (rs imageRoutes) serveThumbnail(w http.ResponseWriter, r *http.Request, img *models.Image) { +func (rs imageRoutes) serveThumbnail(w http.ResponseWriter, r *http.Request, img *models.Image, modTime *time.Time) { mgr := manager.GetInstance() filepath := mgr.Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth) // if the thumbnail doesn't exist, encode on the fly exists, _ := fsutil.FileExists(filepath) if exists { - utils.ServeStaticFile(w, r, filepath) + if modTime == nil { + utils.ServeStaticFile(w, r, filepath) + } else { + utils.ServeStaticFileModTime(w, r, filepath, *modTime) + } } else { const useDefault = true diff --git a/pkg/utils/http.go b/pkg/utils/http.go index d2b40af99..a893a93f3 100644 --- a/pkg/utils/http.go +++ b/pkg/utils/http.go @@ -2,7 +2,10 @@ package utils import ( "bytes" + "errors" + "io/fs" "net/http" + "path/filepath" "time" "github.com/stashapp/stash/pkg/hash/md5" @@ -15,14 +18,18 @@ func GenerateETag(data []byte) string { return `"` + hash + `"` } -// Serves static content, adding Cache-Control: no-cache and a generated ETag header. -// Responds to conditional requests using the ETag. -func ServeStaticContent(w http.ResponseWriter, r *http.Request, data []byte) { +func setStaticContentCacheControl(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Has("t") { w.Header().Set("Cache-Control", "private, max-age=31536000, immutable") } else { w.Header().Set("Cache-Control", "no-cache") } +} + +// Serves static content, adding Cache-Control: no-cache and a generated ETag header. +// Responds to conditional requests using the ETag. +func ServeStaticContent(w http.ResponseWriter, r *http.Request, data []byte) { + setStaticContentCacheControl(w, r) w.Header().Set("ETag", GenerateETag(data)) http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(data)) @@ -31,11 +38,42 @@ func ServeStaticContent(w http.ResponseWriter, r *http.Request, data []byte) { // Serves static content at filepath, adding Cache-Control: no-cache. // Responds to conditional requests using the file modtime. func ServeStaticFile(w http.ResponseWriter, r *http.Request, filepath string) { - if r.URL.Query().Has("t") { - w.Header().Set("Cache-Control", "private, max-age=31536000, immutable") - } else { - w.Header().Set("Cache-Control", "no-cache") - } + setStaticContentCacheControl(w, r) http.ServeFile(w, r, filepath) } + +func toHTTPError(err error) (msg string, httpStatus int) { + if errors.Is(err, fs.ErrNotExist) { + return "404 page not found", http.StatusNotFound + } + if errors.Is(err, fs.ErrPermission) { + return "403 Forbidden", http.StatusForbidden + } + return "500 Internal Server Error", http.StatusInternalServerError +} + +// ServeStaticFileModTime serves a static file at the given path using the given modTime instead of the file modTime. +func ServeStaticFileModTime(w http.ResponseWriter, r *http.Request, path string, modTime time.Time) { + setStaticContentCacheControl(w, r) + + dir, file := filepath.Split(path) + fs := http.Dir(dir) + + f, err := fs.Open(file) + if err != nil { + msg, code := toHTTPError(err) + http.Error(w, msg, code) + return + } + defer f.Close() + + d, err := f.Stat() + if err != nil { + msg, code := toHTTPError(err) + http.Error(w, msg, code) + return + } + + http.ServeContent(w, r, d.Name(), modTime, f) +}