FIX: Make sure S3 object headers are preserved on copy (#14302)

When copying an existing upload stub temporary object
on S3 to its final destination we were not copying across
its additional headers such as content-disposition and
cache-control, which led to issues like attachments not
downloading with their original filename when clicking
the download links in posts.

This is because the metadata_directive = REPLACE option
was not being passed to object.copy_from(), so only the
source object's headers were being used. Added an option
for apply_metadata_to_destination to apply this option
conditionally, because we may not always want to replace
this metadata, but we definitely do when copying a temporary
upload.
This commit is contained in:
Martin Brennan
2021-09-10 12:59:51 +10:00
committed by GitHub
parent f3df5834b6
commit 0d809197aa
4 changed files with 97 additions and 1 deletions

View File

@@ -136,6 +136,49 @@ describe FileStore::S3Store do
end
end
end
describe "#move_existing_stored_upload" do
let(:uploaded_file) { file_from_fixtures(original_filename) }
let(:upload_sha1) { Digest::SHA1.hexdigest(File.read(uploaded_file)) }
let(:original_filename) { "smallest.png" }
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",
apply_metadata_to_destination: true
}
end
let(:external_upload_stub) { Fabricate(:image_external_upload_stub) }
let(:existing_external_upload_key) { external_upload_stub.key }
before do
SiteSetting.authorized_extensions = "pdf|png"
end
it "does not provide a content_disposition for images" do
s3_helper.expects(:copy).with(external_upload_stub.key, kind_of(String), options: upload_opts).returns(["path", "etag"])
s3_helper.expects(:delete_object).with(external_upload_stub.key)
upload = Fabricate(:upload, extension: "png", sha1: upload_sha1, original_filename: original_filename)
store.move_existing_stored_upload(external_upload_stub.key, upload, "image/png")
end
context "when the file is a PDF" do
let(:external_upload_stub) { Fabricate(:attachment_external_upload_stub, original_filename: original_filename) }
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}\"; filename*=UTF-8''#{original_filename}", content_type: "application/pdf" }
s3_helper.expects(:copy).with(external_upload_stub.key, kind_of(String), options: upload_opts.merge(disp_opts)).returns(["path", "etag"])
upload = Fabricate(:upload, extension: "png", sha1: upload_sha1, original_filename: original_filename)
store.move_existing_stored_upload(external_upload_stub.key, upload, "application/pdf")
end
end
end
end
context 'copying files in S3' do

View File

@@ -95,4 +95,38 @@ describe "S3Helper" do
expect(object1.key).to eq("folder_path/original/1X/def.xyz")
expect(object2.key).to eq("#{FileStore::BaseStore::TEMPORARY_UPLOAD_PREFIX}folder_path/uploads/default/blah/def.xyz")
end
describe "#copy" do
let(:source_key) { "#{FileStore::BaseStore::TEMPORARY_UPLOAD_PREFIX}uploads/default/blah/source.jpg" }
let(:destination_key) { "original/1X/destination.jpg" }
let(:s3_helper) { S3Helper.new("test-bucket", "", client: client) }
it "can copy an object from the source to the destination" do
destination_stub = Aws::S3::Object.new(bucket_name: "test-bucket", key: destination_key, client: client)
s3_helper.send(:s3_bucket).expects(:object).with(destination_key).returns(destination_stub)
destination_stub.expects(:copy_from).with(copy_source: "test-bucket/#{source_key}").returns(
stub(copy_object_result: stub(etag: "\"etag\""))
)
response = s3_helper.copy(source_key, destination_key)
expect(response.first).to eq(destination_key)
expect(response.second).to eq("etag")
end
it "puts the metadata from options onto the destination if apply_metadata_to_destination" do
content_disposition = "attachment; filename=\"source.jpg\"; filename*=UTF-8''source.jpg"
destination_stub = Aws::S3::Object.new(bucket_name: "test-bucket", key: destination_key, client: client)
s3_helper.send(:s3_bucket).expects(:object).with(destination_key).returns(destination_stub)
destination_stub.expects(:copy_from).with(
copy_source: "test-bucket/#{source_key}", content_disposition: content_disposition, metadata_directive: "REPLACE"
).returns(
stub(copy_object_result: stub(etag: "\"etag\""))
)
response = s3_helper.copy(
source_key, destination_key,
options: { apply_metadata_to_destination: true, content_disposition: content_disposition }
)
expect(response.first).to eq(destination_key)
expect(response.second).to eq("etag")
end
end
end