diff --git a/CHANGELOG.md b/CHANGELOG.md index 6144f929..12ce2b98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,8 @@ - **!** renamed `models.RequestData` to `models.RequestInfo` and soft-deprecated `apis.RequestData(c)` to `apis.RequestInfo(c)` to avoid the stuttering with the `Data` field. _The old `apis.RequestData()` method still works to minimize the breaking changes but it is recommended to replace it with `apis.RequestInfo(c)`._ +- Added `?download` file query parameter option to instruct the browser to always download a file and not show a preview. + ## v0.16.10 diff --git a/tools/filesystem/filesystem.go b/tools/filesystem/filesystem.go index 59601efa..c31e45f6 100644 --- a/tools/filesystem/filesystem.go +++ b/tools/filesystem/filesystem.go @@ -321,7 +321,14 @@ var manualExtensionContentTypes = map[string]string{ ".css": "text/css", // (see https://github.com/gabriel-vasile/mimetype/pull/113) } +// forceAttachmentParam is the name of the request query parameter to +// force "Content-Disposition: attachment" header. +const forceAttachmentParam = "download" + // Serve serves the file at fileKey location to an HTTP response. +// +// If the `download` query parameter is used the file will be always served for +// download no matter of its type (aka. with "Content-Disposition: attachment"). func (s *System) Serve(res http.ResponseWriter, req *http.Request, fileKey string, name string) error { br, readErr := s.bucket.NewReader(s.ctx, fileKey, nil) if readErr != nil { @@ -329,9 +336,11 @@ func (s *System) Serve(res http.ResponseWriter, req *http.Request, fileKey strin } defer br.Close() + forceAttachment := req.URL.Query().Has(forceAttachmentParam) + disposition := "attachment" realContentType := br.ContentType() - if list.ExistInSlice(realContentType, inlineServeContentTypes) { + if !forceAttachment && list.ExistInSlice(realContentType, inlineServeContentTypes) { disposition = "inline" } diff --git a/tools/filesystem/filesystem_test.go b/tools/filesystem/filesystem_test.go index c2b51bad..ce81a05a 100644 --- a/tools/filesystem/filesystem_test.go +++ b/tools/filesystem/filesystem_test.go @@ -257,7 +257,8 @@ func TestFileSystemServe(t *testing.T) { scenarios := []struct { path string name string - customHeaders map[string]string + query map[string]string + headers map[string]string expectError bool expectHeaders map[string]string }{ @@ -266,6 +267,7 @@ func TestFileSystemServe(t *testing.T) { "missing.txt", "test_name.txt", nil, + nil, true, nil, }, @@ -274,6 +276,7 @@ func TestFileSystemServe(t *testing.T) { "test/sub1.txt", "test_name.txt", nil, + nil, false, map[string]string{ "Content-Disposition": "attachment; filename=test_name.txt", @@ -288,6 +291,7 @@ func TestFileSystemServe(t *testing.T) { "image.png", "test_name.png", nil, + nil, false, map[string]string{ "Content-Disposition": "inline; filename=test_name.png", @@ -297,11 +301,27 @@ func TestFileSystemServe(t *testing.T) { "Cache-Control": cacheControl, }, }, + { + // png with forced attachment + "image.png", + "test_name_download.png", + map[string]string{"download": "12"}, + nil, + false, + map[string]string{ + "Content-Disposition": "attachment; filename=test_name_download.png", + "Content-Type": "image/png", + "Content-Length": "73", + "Content-Security-Policy": csp, + "Cache-Control": cacheControl, + }, + }, { // svg exception "image.svg", "test_name.svg", nil, + nil, false, map[string]string{ "Content-Disposition": "attachment; filename=test_name.svg", @@ -316,6 +336,7 @@ func TestFileSystemServe(t *testing.T) { "style.css", "test_name.css", nil, + nil, false, map[string]string{ "Content-Disposition": "attachment; filename=test_name.css", @@ -329,6 +350,7 @@ func TestFileSystemServe(t *testing.T) { // custom header "test/sub2.txt", "test_name.txt", + nil, map[string]string{ "Content-Disposition": "1", "Content-Type": "2", @@ -353,7 +375,13 @@ func TestFileSystemServe(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) - for k, v := range s.customHeaders { + query := req.URL.Query() + for k, v := range s.query { + query.Set(k, v) + } + req.URL.RawQuery = query.Encode() + + for k, v := range s.headers { res.Header().Set(k, v) }