refactor(files): extract WriteFileDownload shared by attachment download
Split the generic file-download writer (ServeContent for seekable readers, manual 304 + io.Copy otherwise) out of WriteAttachmentDownload so other blob endpoints can reuse it. The attachment writer keeps its preview branch and cache override and delegates the rest.
This commit is contained in:
parent
cf456fb223
commit
ca4e747bed
|
|
@ -0,0 +1,57 @@
|
||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/files"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WriteFileDownload streams a loaded file (its .File reader must be open) to the
|
||||||
|
// response as an attachment download: http.ServeContent for seekable local files
|
||||||
|
// (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) {
|
||||||
|
mimeToReturn := f.Mime
|
||||||
|
if mimeToReturn == "" {
|
||||||
|
mimeToReturn = "application/octet-stream"
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": f.Name}))
|
||||||
|
w.Header().Set("Content-Type", mimeToReturn)
|
||||||
|
w.Header().Set("Content-Length", strconv.FormatUint(f.Size, 10))
|
||||||
|
w.Header().Set("Last-Modified", f.Created.UTC().Format(http.TimeFormat))
|
||||||
|
|
||||||
|
// Local files are *os.File (seekable), so ServeContent gives Range +
|
||||||
|
// If-Modified-Since for free; s3 (and the in-memory test storage) return a
|
||||||
|
// non-seekable reader, so check If-Modified-Since manually and io.Copy.
|
||||||
|
if seeker, ok := f.File.(io.ReadSeeker); ok {
|
||||||
|
http.ServeContent(w, r, f.Name, f.Created, seeker)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ifModSince := r.Header.Get("If-Modified-Since"); ifModSince != "" {
|
||||||
|
if t, parseErr := http.ParseTime(ifModSince); parseErr == nil && !f.Created.UTC().After(t) {
|
||||||
|
w.WriteHeader(http.StatusNotModified)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _ = io.Copy(w, f.File)
|
||||||
|
}
|
||||||
|
|
@ -21,8 +21,6 @@
|
||||||
package files
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
|
@ -62,9 +60,9 @@ func toAttachmentUploadError(err error) AttachmentUploadError {
|
||||||
return AttachmentUploadError{Message: err.Error()}
|
return AttachmentUploadError{Message: err.Error()}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteAttachmentDownload streams the attachment (or its preview) to the response:
|
// WriteAttachmentDownload streams the attachment (or its inline image preview) to
|
||||||
// http.ServeContent for seekable local files (Range + If-Modified-Since for free),
|
// the response and closes the file reader. The non-preview path delegates to
|
||||||
// a manual 304 + io.Copy otherwise. It closes the file reader.
|
// WriteFileDownload, adding the cache override that lets browsers cache attachments.
|
||||||
func WriteAttachmentDownload(w http.ResponseWriter, r *http.Request, ta *models.TaskAttachment, preview []byte) {
|
func WriteAttachmentDownload(w http.ResponseWriter, r *http.Request, ta *models.TaskAttachment, preview []byte) {
|
||||||
defer func() { _ = ta.File.File.Close() }()
|
defer func() { _ = ta.File.File.Close() }()
|
||||||
|
|
||||||
|
|
@ -75,30 +73,7 @@ func WriteAttachmentDownload(w http.ResponseWriter, r *http.Request, ta *models.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mimeToReturn := ta.File.Mime
|
|
||||||
if mimeToReturn == "" {
|
|
||||||
mimeToReturn = "application/octet-stream"
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": ta.File.Name}))
|
|
||||||
w.Header().Set("Content-Type", mimeToReturn)
|
|
||||||
w.Header().Set("Content-Length", strconv.FormatUint(ta.File.Size, 10))
|
|
||||||
w.Header().Set("Last-Modified", ta.File.Created.UTC().Format(http.TimeFormat))
|
|
||||||
// Override the global no-store directive so browsers can cache attachments.
|
// Override the global no-store directive so browsers can cache attachments.
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
WriteFileDownload(w, r, ta.File)
|
||||||
// Local files are *os.File (seekable), so ServeContent gives Range +
|
|
||||||
// If-Modified-Since for free; s3 (and the in-memory test storage) return a
|
|
||||||
// non-seekable reader, so check If-Modified-Since manually and io.Copy.
|
|
||||||
if seeker, ok := ta.File.File.(io.ReadSeeker); ok {
|
|
||||||
http.ServeContent(w, r, ta.File.Name, ta.File.Created, seeker)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ifModSince := r.Header.Get("If-Modified-Since"); ifModSince != "" {
|
|
||||||
if t, parseErr := http.ParseTime(ifModSince); parseErr == nil && !ta.File.Created.UTC().After(t) {
|
|
||||||
w.WriteHeader(http.StatusNotModified)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, _ = io.Copy(w, ta.File.File)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue