Skip to content

Commit

Permalink
FIX: Add attachment content-disposition for all non-image files (#10058)
Browse files Browse the repository at this point in the history
This will make it so the original filename is used when downloading all non-image files, bringing S3Store into line with the to_s3 migration and local storage. Video and audio files will still stream correctly in HTML players as well.

See https://meta.discourse.org/t/cannot-download-non-image-media-files-original-filenames-lost-when-uploaded-to-s3/152797 for a lot of extra context.
  • Loading branch information
martin-brennan committed Jun 17, 2020
1 parent dcb816b commit e5da2d2
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 4 deletions.
10 changes: 8 additions & 2 deletions lib/file_store/s3_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,14 @@ def store_file(file, path, opts = {})
cache_control: 'max-age=31556952, public, immutable',
content_type: opts[:content_type].presence || MiniMime.lookup_by_filename(filename)&.content_type
}
# add a "content disposition" header for "attachments"
options[:content_disposition] = "attachment; filename=\"#{filename}\"" unless FileHelper.is_supported_media?(filename)

# add a "content disposition: attachment" header with the original filename
# for everything but images. audio and video will still stream correctly in
# HTML players, and when a direct link is provided to any file but an image
# it will download correctly in the browser.
if !FileHelper.is_supported_image?(filename)
options[:content_disposition] = "attachment; filename=\"#{filename}\""
end

path.prepend(File.join(upload_path, "/")) if Rails.configuration.multisite

Expand Down
Binary file added spec/fixtures/media/small.mp3
Binary file not shown.
Binary file added spec/fixtures/media/small.mp4
Binary file not shown.
54 changes: 52 additions & 2 deletions spec/multisite/s3_store_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
require 'file_store/s3_store'

RSpec.describe 'Multisite s3 uploads', type: :multisite do
let(:uploaded_file) { file_from_fixtures("smallest.png") }
let(:original_filename) { "smallest.png" }
let(:uploaded_file) { file_from_fixtures(original_filename) }
let(:upload_sha1) { Digest::SHA1.hexdigest(File.read(uploaded_file)) }
let(:upload_path) { Discourse.store.upload_path }

def build_upload
Fabricate.build(:upload, sha1: upload_sha1, id: 1)
Fabricate.build(:upload, sha1: upload_sha1, id: 1, original_filename: original_filename)
end

context 'uploading to s3' do
Expand All @@ -24,6 +25,55 @@ def build_upload
let(:s3_client) { Aws::S3::Client.new(stub_responses: true) }
let(:s3_helper) { S3Helper.new(SiteSetting.s3_upload_bucket, '', client: s3_client) }
let(:store) { FileStore::S3Store.new(s3_helper) }
let(:upload_opts) do
{
acl: "public-read",
cache_control: "max-age=31556952, public, immutable",
content_type: "image/png"
}
end

it "does not provide a content_disposition for images" do
s3_helper.expects(:upload).with(uploaded_file, kind_of(String), upload_opts).returns(["path", "etag"])
upload = build_upload
store.store_upload(uploaded_file, upload)
end

context "when the file is a PDF" do
let(:original_filename) { "small.pdf" }
let(:uploaded_file) { file_from_fixtures("small.pdf", "pdf") }

it "adds an attachment content-disposition with the original filename" do
disp_opts = { content_disposition: "attachment; filename=\"#{original_filename}\"", content_type: "application/pdf" }
s3_helper.expects(:upload).with(uploaded_file, kind_of(String), upload_opts.merge(disp_opts)).returns(["path", "etag"])
upload = build_upload
store.store_upload(uploaded_file, upload)
end
end

context "when the file is a video" do
let(:original_filename) { "small.mp4" }
let(:uploaded_file) { file_from_fixtures("small.mp4", "media") }

it "adds an attachment content-disposition with the original filename" do
disp_opts = { content_disposition: "attachment; filename=\"#{original_filename}\"", content_type: "application/mp4" }
s3_helper.expects(:upload).with(uploaded_file, kind_of(String), upload_opts.merge(disp_opts)).returns(["path", "etag"])
upload = build_upload
store.store_upload(uploaded_file, upload)
end
end

context "when the file is audio" do
let(:original_filename) { "small.mp3" }
let(:uploaded_file) { file_from_fixtures("small.mp3", "media") }

it "adds an attachment content-disposition with the original filename" do
disp_opts = { content_disposition: "attachment; filename=\"#{original_filename}\"", content_type: "audio/mpeg" }
s3_helper.expects(:upload).with(uploaded_file, kind_of(String), upload_opts.merge(disp_opts)).returns(["path", "etag"])
upload = build_upload
store.store_upload(uploaded_file, upload)
end
end

it "returns the correct url for default and second multisite db" do
test_multisite_connection('default') do
Expand Down

1 comment on commit e5da2d2

@discoursebot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit has been mentioned on Discourse Meta. There might be relevant details there:

https://meta.discourse.org/t/broken-pipe-error-when-uploading-to-a-s3-clone-a-pdf-with-a-name-containing-e-i-etc/155414/2

Please sign in to comment.