gems/aws-sdk-s3/spec/object/upload_file_spec.rb (251 lines of code) (raw):
# frozen_string_literal: true
require_relative '../spec_helper'
require 'tempfile'
module Aws
module S3
describe Object do
RSpec::Matchers.define :file_part do |expected|
match do |actual|
actual.source == expected[:source] &&
actual.first_byte == expected[:offset] &&
actual.last_byte == expected[:offset] + expected[:size] &&
actual.size == expected[:size]
end
end
let(:client) { S3::Client.new(stub_responses: true) }
describe '#upload_file' do
let(:one_meg) { 1024 * 1024 }
let(:object) do
S3::Object.new(
bucket_name: 'bucket',
key: 'key',
client: client
)
end
let(:one_mb) { '.' * 1024 * 1024 }
let(:one_meg_file) do
Tempfile.new('one-meg-file').tap do |f|
f.write(one_mb)
f.rewind
end
end
let(:ten_meg_file) do
Tempfile.new('ten-meg-file').tap do |f|
10.times { f.write(one_mb) }
f.rewind
end
end
let(:one_hundred_seventeen_meg_file) do
Tempfile.new('one-hundred-seventeen-meg-file').tap do |f|
117.times { f.write(one_mb) }
f.rewind
end
end
it 'uploads objects with custom options without mutating them' do
options = {}.freeze
expect(client).to receive(:put_object).with({
bucket: 'bucket',
key: 'key',
body: one_meg_file
})
object.upload_file(one_meg_file, options)
end
it 'yields the response to the given block' do
object.upload_file(ten_meg_file) do |response|
expect(response).to be_kind_of(Seahorse::Client::Response)
expect(response.etag).to eq('ETag')
end
end
context 'small objects' do
it 'uploads small objects using Client#put_object' do
expect(client).to receive(:put_object).with({
bucket: 'bucket',
key: 'key',
body: ten_meg_file
})
object.upload_file(ten_meg_file)
end
it 'reports progress for small objects' do
expect(client).to receive(:put_object).with({
bucket: 'bucket',
key: 'key',
body: ten_meg_file,
on_chunk_sent: instance_of(Proc)
}) do |args|
args[:on_chunk_sent].call(ten_meg_file, ten_meg_file.size, ten_meg_file.size)
end
callback = proc do |bytes, totals|
expect(bytes).to eq([ten_meg_file.size])
expect(totals).to eq([ten_meg_file.size])
end
object.upload_file(ten_meg_file, progress_callback: callback)
end
it 'accepts an alternative multipart file threshold' do
expect(client).to receive(:put_object).with({
bucket: 'bucket',
key: 'key',
body: one_hundred_seventeen_meg_file
})
object.upload_file(
one_hundred_seventeen_meg_file,
multipart_threshold: 200 * one_meg
)
end
it 'accepts paths to files to upload' do
file = double('file')
expect(File).to receive(:open)
.with(ten_meg_file.path, 'rb').and_yield(file)
expect(client).to receive(:put_object).with({
bucket: 'bucket',
key: 'key',
body: file
})
object.upload_file(ten_meg_file.path)
end
it 'does not fail when given :thread_count' do
expect(client).to receive(:put_object).with({
bucket: 'bucket',
key: 'key',
body: ten_meg_file
})
object.upload_file(ten_meg_file, thread_count: 1)
end
end
context 'large objects' do
it 'uses multipart APIs for objects >= 100MB' do
client.stub_responses(:create_multipart_upload, upload_id: 'id')
client.stub_responses(:upload_part, etag: 'etag', checksum_crc32: 'checksum')
expect(client).to receive(:complete_multipart_upload).with(
bucket: 'bucket',
key: 'key',
upload_id: 'id',
multipart_upload: {
parts: [
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 1 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 2 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 3 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 4 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 5 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 6 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 7 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 8 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 9 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 10 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 11 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 12 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 13 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 14 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 15 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 16 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 17 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 18 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 19 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 20 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 21 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 22 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 23 },
{ checksum_crc32: 'checksum', etag: 'etag', part_number: 24 }
]
}
)
object.upload_file(one_hundred_seventeen_meg_file, content_type: 'text/plain')
end
it 'allows for full object checksums' do
expect(client).to receive(:create_multipart_upload)
.with({bucket: 'bucket', key: 'key', checksum_algorithm: 'CRC32',
checksum_type: 'FULL_OBJECT', content_type: 'text/plain'})
.and_call_original
expect(client).to receive(:upload_part)
.with(hash_not_including(checksum_crc32: anything)).exactly(24).times
.and_call_original
expect(client).to receive(:complete_multipart_upload)
.with(hash_including(checksum_type: 'FULL_OBJECT', checksum_crc32: 'checksum'))
.and_call_original
client.stub_responses(:create_multipart_upload, upload_id: 'id')
client.stub_responses(:upload_part, etag: 'etag', checksum_crc32: 'part')
object.upload_file(one_hundred_seventeen_meg_file, content_type: 'text/plain', checksum_crc32: 'checksum')
end
it 'reports progress for multipart uploads' do
thread = double(value: nil)
allow(Thread).to receive(:new).and_yield.and_return(thread)
client.stub_responses(:create_multipart_upload, upload_id: 'id')
client.stub_responses(:complete_multipart_upload)
expect(client).to receive(:upload_part).exactly(24).times do |args|
args[:on_chunk_sent].call(args[:body], args[:body].size, args[:body].size)
double(
context: double(params: { checksum_algorithm: 'crc32' }),
checksum_crc32: 'checksum',
etag: 'etag'
)
end
callback = proc do |bytes, totals|
expect(bytes.size).to eq(24)
expect(totals.size).to eq(24)
end
object.upload_file(one_hundred_seventeen_meg_file, content_type: 'text/plain', progress_callback: callback)
end
it 'defaults to THREAD_COUNT without the thread_count option' do
expect(Thread).to receive(:new).exactly(S3::MultipartFileUploader::THREAD_COUNT).times.and_return(double(value: nil))
client.stub_responses(:create_multipart_upload, upload_id: 'id')
client.stub_responses(:complete_multipart_upload)
object.upload_file(one_hundred_seventeen_meg_file)
end
it 'respects the thread_count option' do
custom_thread_count = 20
expect(Thread).to receive(:new).exactly(custom_thread_count).times.and_return(double(value: nil))
client.stub_responses(:create_multipart_upload, upload_id: 'id')
client.stub_responses(:complete_multipart_upload)
object.upload_file(one_hundred_seventeen_meg_file, thread_count: custom_thread_count)
end
it 'raises an error if the multipart threshold is too small' do
error_msg = 'unable to multipart upload files smaller than 5MB'
expect do
object.upload_file(one_meg_file, multipart_threshold: one_meg)
end.to raise_error(ArgumentError, error_msg)
end
it 'automatically deletes failed multipart upload on error' do
allow_any_instance_of(FilePart).to receive(:read).and_return(nil)
client.stub_responses(
:upload_part,
[
{ etag: 'etag-1' },
{ etag: 'etag-2' },
RuntimeError.new('part 3 failed'),
{ etag: 'etag-4' }
]
)
expect(client).to receive(:abort_multipart_upload).with(
bucket: 'bucket',
key: 'key',
upload_id: 'MultipartUploadId'
)
expect do
object.upload_file(one_hundred_seventeen_meg_file)
end.to raise_error('multipart upload failed: part 3 failed')
end
it 'reports when it is unable to abort a failed multipart upload' do
allow(Thread).to receive(:new) do |_, &block|
double(value: block.call)
end
client.stub_responses(
:upload_part,
[
{ etag: 'etag-1' },
{ etag: 'etag-2' },
{ etag: 'etag-3' },
RuntimeError.new('part failed')
]
)
client.stub_responses(
:abort_multipart_upload,
[RuntimeError.new('network-error')]
)
expect { object.upload_file(one_hundred_seventeen_meg_file) }.to raise_error(
S3::MultipartUploadError,
/failed to abort multipart upload: network-error. Multipart upload failed: part failed/
)
end
end
end
end
end
end