fix(files): never cache file downloads in v1 or v2

Move the Cache-Control: no-cache header into the shared WriteFileDownload
so every export and attachment download carries it, and add it to the
standalone v1 export download writer too. Downloads must never be cached.
This commit is contained in:
kolaente 2026-06-17 14:24:41 +02:00 committed by kolaente
parent ee8dbf82ba
commit 4b92f23329
4 changed files with 12 additions and 3 deletions

View File

@ -132,6 +132,10 @@ func DownloadUserDataExport(c *echo.Context) error {
return err
}
// Downloads must never be cached; no-cache overrides the global no-store
// directive while still allowing revalidation.
c.Response().Header().Set("Cache-Control", "no-cache")
if config.FilesType.GetString() == "s3" {
c.Response().Header().Set("Content-Disposition", "attachment; filename=\""+exportFile.Name+"\"")
c.Response().Header().Set("Content-Type", exportFile.Mime)

View File

@ -30,6 +30,10 @@ import (
// (Range + If-Modified-Since for free), a manual 304 + io.Copy otherwise. It does
// not close the reader; the caller owns it.
func WriteFileDownload(w http.ResponseWriter, r *http.Request, f *files.File) {
// Downloads must never be cached. no-cache overrides the global no-store
// directive so revalidation (If-Modified-Since) still works.
w.Header().Set("Cache-Control", "no-cache")
mimeToReturn := f.Mime
if mimeToReturn == "" {
mimeToReturn = "application/octet-stream"

View File

@ -62,18 +62,18 @@ func toAttachmentUploadError(err error) AttachmentUploadError {
// WriteAttachmentDownload streams the attachment (or its inline image preview) to
// the response and closes the file reader. The non-preview path delegates to
// WriteFileDownload, adding the cache override that lets browsers cache attachments.
// WriteFileDownload, which sets Cache-Control: no-cache; the preview branch returns
// early, so it sets the same header itself.
func WriteAttachmentDownload(w http.ResponseWriter, r *http.Request, ta *models.TaskAttachment, preview []byte) {
defer func() { _ = ta.File.File.Close() }()
if preview != nil {
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Content-Length", strconv.Itoa(len(preview)))
_, _ = w.Write(preview)
return
}
// Override the global no-store directive so browsers can cache attachments.
w.Header().Set("Cache-Control", "no-cache")
WriteFileDownload(w, r, ta.File)
}

View File

@ -72,6 +72,7 @@ func TestHumaUserExport(t *testing.T) {
require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String())
assert.Equal(t, content, rec.Body.Bytes(), "the streamed export bytes must match")
assert.Contains(t, rec.Header().Get("Content-Disposition"), "test")
assert.Equal(t, "no-cache", rec.Header().Get("Cache-Control"), "downloads must never be cached")
})
t.Run("Download with a wrong password is refused", func(t *testing.T) {