From 54d977532e9e9a99281bc56965583d07f3913b21 Mon Sep 17 00:00:00 2001 From: Weijie Zhao Date: Thu, 5 Mar 2026 00:43:03 +0800 Subject: [PATCH] fix: allow browser caching for file downloads (#2349) --- pkg/modules/background/handler/background.go | 14 +++++++++++++- pkg/routes/api/v1/task_attachment.go | 11 +++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pkg/modules/background/handler/background.go b/pkg/modules/background/handler/background.go index 2f405c8e5..7049027cd 100644 --- a/pkg/modules/background/handler/background.go +++ b/pkg/modules/background/handler/background.go @@ -380,9 +380,21 @@ func GetProjectBackground(c *echo.Context) error { return err } + // Override the global no-store directive so browsers can cache background images. + // no-cache allows caching but requires revalidation via If-Modified-Since. + c.Response().Header().Set("Cache-Control", "no-cache") + // Set Last-Modified header if we have the file stat, so clients can decide whether to use cached files if stat != nil { - c.Response().Header().Set(echo.HeaderLastModified, stat.ModTime().UTC().Format(http.TimeFormat)) + modTime := stat.ModTime().UTC() + c.Response().Header().Set(echo.HeaderLastModified, modTime.Format(http.TimeFormat)) + + // Check If-Modified-Since and return 304 if the file hasn't changed + if ifModSince := c.Request().Header.Get("If-Modified-Since"); ifModSince != "" { + if t, err := http.ParseTime(ifModSince); err == nil && !modTime.After(t) { + return c.NoContent(http.StatusNotModified) + } + } } // Serve the file diff --git a/pkg/routes/api/v1/task_attachment.go b/pkg/routes/api/v1/task_attachment.go index 0bb10c903..c85b67afd 100644 --- a/pkg/routes/api/v1/task_attachment.go +++ b/pkg/routes/api/v1/task_attachment.go @@ -217,8 +217,19 @@ func GetTaskAttachment(c *echo.Context) error { c.Response().Header().Set("Content-Type", mimeToReturn) c.Response().Header().Set("Content-Length", strconv.FormatUint(taskAttachment.File.Size, 10)) c.Response().Header().Set("Last-Modified", taskAttachment.File.Created.UTC().Format(http.TimeFormat)) + // Override the global no-store directive so browsers can cache attachments. + // no-cache allows caching but requires revalidation via If-Modified-Since. + c.Response().Header().Set("Cache-Control", "no-cache") if config.FilesType.GetString() == "s3" { + // Check If-Modified-Since and return 304 if the file hasn't changed. + // http.ServeContent handles this automatically for local files. + if ifModSince := c.Request().Header.Get("If-Modified-Since"); ifModSince != "" { + if t, parseErr := http.ParseTime(ifModSince); parseErr == nil && !taskAttachment.File.Created.UTC().After(t) { + return c.NoContent(http.StatusNotModified) + } + } + // s3 files cannot use http.ServeContent as it requires a Seekable file // so we stream the file content directly to the response _, err = io.Copy(c.Response(), taskAttachment.File.File)