gems/aws-sdk-s3/spec/object/multipart_copy_spec.rb (418 lines of code) (raw):
# frozen_string_literal: true
require_relative '../spec_helper'
module Aws
module S3
describe Object do
let(:object) do
S3::Object.new('bucket', 'unescaped/key path', stub_responses: true)
end
let(:client) { object.client }
def get_requests(cli, operation_name)
cli.api_requests.select { |req| req[:operation_name] == operation_name.to_sym }
end
def get_request_params(cli, operation_name)
get_requests(cli, operation_name).first[:params]
end
describe '#copy_to' do
it 'accepts a string source' do
object.copy_to('target-bucket/target-key')
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({ bucket: 'target-bucket', key: 'target-key', copy_source: 'bucket/unescaped/key%20path' })
)
end
it 'accepts a hash source' do
object.copy_to(bucket: 'target-bucket', key: 'target-key')
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({ bucket: 'target-bucket', key: 'target-key', copy_source: 'bucket/unescaped/key%20path' })
)
end
it 'accepts a hash source' do
object.copy_to(bucket: 'target-bucket', key: 'target-key')
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({ bucket: 'target-bucket', key: 'target-key', copy_source: 'bucket/unescaped/key%20path' })
)
end
it 'accept a hash with options merged' do
object.copy_to(bucket: 'target-bucket', key: 'target-key', content_type: 'text/plain')
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({
bucket: 'target-bucket',
key: 'target-key',
copy_source: 'bucket/unescaped/key%20path',
content_type: 'text/plain'
})
)
end
it 'accepts an S3::Object source' do
target = S3::Object.new('target-bucket', 'target-key', stub_responses: true)
object.copy_to(target)
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({ bucket: 'target-bucket', key: 'target-key', copy_source: 'bucket/unescaped/key%20path' })
)
end
it 'accepts additional options' do
object.copy_to('target-bucket/target-key', acl: 'public-read')
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({
bucket: 'target-bucket',
key: 'target-key',
copy_source: 'bucket/unescaped/key%20path',
acl: 'public-read'
})
)
end
it 'raises an error on an invalid targets' do
expect { object.copy_to(:target) }.to raise_error(ArgumentError)
end
end
describe '#copy_from' do
context 'with multipart_copy: false' do
it 'supports the deprecated form' do
object.copy_from(copy_source: 'source-bucket/escaped/source/key%20path')
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({
bucket: 'bucket',
key: 'unescaped/key path',
copy_source: 'source-bucket/escaped/source/key%20path'
})
)
end
it 'accepts a string source' do
object.copy_from('source-bucket/source/key%20path')
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({
bucket: 'bucket',
key: 'unescaped/key path',
copy_source: 'source-bucket/source/key%20path'
})
)
end
it 'accepts a hash source' do
object.copy_from(bucket: 'source-bucket', key: 'unescaped/source/key path')
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({
bucket: 'bucket',
key: 'unescaped/key path',
copy_source: 'source-bucket/unescaped/source/key%20path'
})
)
end
it 'accepts a hash source with version id' do
object.copy_from(bucket: 'src-bucket', key: 'src key', version_id: 'src-version-id')
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({
bucket: 'bucket',
key: 'unescaped/key path',
copy_source: 'src-bucket/src%20key?versionId=src-version-id'
})
)
end
it 'accept a hash with options merged' do
object.copy_from(bucket: 'source-bucket', key: 'source key', content_type: 'text/plain')
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({
bucket: 'bucket',
key: 'unescaped/key path',
copy_source: 'source-bucket/source%20key',
content_type: 'text/plain'
})
)
end
it 'accepts an S3::Object source' do
src = S3::Object.new('source-bucket', 'unescaped/source/key path', stub_responses: true)
object.copy_from(src)
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({
bucket: 'bucket',
key: 'unescaped/key path',
copy_source: 'source-bucket/unescaped/source/key%20path'
})
)
end
it 'accepts an S3::ObjectSummary source' do
src = S3::ObjectSummary.new('source-bucket', 'unescaped/source/key path', stub_responses: true)
object.copy_from(src)
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({
bucket: 'bucket',
key: 'unescaped/key path',
copy_source: 'source-bucket/unescaped/source/key%20path'
})
)
end
it 'accepts an S3::ObjectVersion source' do
src = S3::ObjectVersion.new(
'source-bucket', 'unescaped/source/key path',
'source-version-id',
stub_responses: true
)
object.copy_from(src)
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({
bucket: 'bucket',
key: 'unescaped/key path',
copy_source: 'source-bucket/unescaped/source/key%20path?versionId=source-version-id'
})
)
end
it 'accepts additional options' do
object.copy_from('source-bucket/source%20key', acl: 'public-read')
expect(get_requests(client, :copy_object).size).to eq(1)
expect(get_request_params(client, :copy_object)).to(
eq({
bucket: 'bucket',
key: 'unescaped/key path',
copy_source: 'source-bucket/source%20key',
acl: 'public-read'
})
)
end
it 'raises an error on an invalid source' do
expect { object.copy_from(:source) }.to raise_error(ArgumentError)
end
end
context 'with version_id and multipart_copy: true' do
before(:each) do
client.stub_responses(
:head_object,
client.stub_data(
:head_object,
content_length: 300 * 1024 * 1024, # 300MB
content_type: 'application/json',
server_side_encryption: 'aws:kms',
ssekms_key_id: 'arn:aws:kms:us-east-1:1234567890:key/00000000-0000-0000-0000-000000000000'
)
)
end
it 'performs multipart uploads for a versioned object' do
source = 'source-bucket/source%20key?versionId=source-version-id'
object.copy_from(source, multipart_copy: true)
expect(get_requests(client, :create_multipart_upload).size).to eq(1)
expect(create_req = get_request_params(client, :create_multipart_upload)).to(
include({ bucket: 'bucket', key: 'unescaped/key path', content_type: 'application/json' })
)
expect(create_req).not_to include(server_side_encryption: anything, ssekms_key_id: anything)
expect((requests = get_requests(client, :upload_part_copy).map { |req| req[:params] }).size).to eq(6)
requests.sort_by { |req| req[:part_number] }.each.with_index do |part, i|
n = i + 1
range = "bytes=#{(n - 1) * 52_428_800}-#{n * 52_428_800 - 1}"
expect(part).to eq({ bucket: 'bucket',
key: 'unescaped/key path',
part_number: n,
copy_source: source,
copy_source_range: range,
upload_id: 'MultipartUploadId' })
end
expect(get_requests(client, :complete_multipart_upload).size).to eq(1)
expect(get_request_params(client, :complete_multipart_upload)).to(
include({ bucket: 'bucket',
key: 'unescaped/key path',
upload_id: 'MultipartUploadId',
multipart_upload: { parts: (1..6).map { |n| a_hash_including({ part_number: n }) } } })
)
end
it 'supports alternative part sizes' do
object.copy_from('source-bucket/source%20key', multipart_copy: true, min_part_size: 5 * 1024 * 1024)
expect(get_requests(client, :create_multipart_upload).size).to eq(1)
expect(get_request_params(client, :create_multipart_upload)).to(
include({ bucket: 'bucket', key: 'unescaped/key path', content_type: 'application/json' })
)
expect((requests = get_requests(client, :upload_part_copy).map { |req| req[:params] }).size).to eq(60)
requests.sort_by { |r| r[:part_number] }.each.with_index do |part, i|
n = i + 1
range = "bytes=#{(n - 1) * 5_242_880}-#{n * 5_242_880 - 1}"
expect(part).to eq({ bucket: 'bucket',
key: 'unescaped/key path',
part_number: n,
copy_source: 'source-bucket/source%20key',
copy_source_range: range,
upload_id: 'MultipartUploadId' })
end
expect(get_requests(client, :complete_multipart_upload).size).to eq(1)
expect(get_request_params(client, :complete_multipart_upload)).to(
include({
bucket: 'bucket',
key: 'unescaped/key path',
upload_id: 'MultipartUploadId',
multipart_upload: { parts: (1..60).map { |n| a_hash_including({ part_number: n }) } }
})
)
end
it 'aborts the upload on errors', thread_report_on_exception: false do
client.stub_responses(:upload_part_copy, Array.new(10, 'NoSuchKey'))
expect do
object.copy_from('source-bucket/source%20key', multipart_copy: true)
end.to raise_error(Aws::S3::Errors::NoSuchKey)
expect(get_requests(client, :abort_multipart_upload).size).to eq(1)
expect(get_request_params(client, :abort_multipart_upload)).to(
eq({ bucket: 'bucket', key: 'unescaped/key path', upload_id: 'MultipartUploadId' })
)
end
it 'rejects files smaller than 5MB' do
client.stub_responses(:head_object, client.stub_data(:head_object, content_length: 4 * 1024 * 1024)) # 4MB
expect do
object.copy_from('source-bucket/source%20key', multipart_copy: true)
end.to raise_error(ArgumentError, /smaller than 5MB/)
end
it 'accepts file size option to avoid HEAD request' do
object.copy_from('source-bucket/source%20key', multipart_copy: true, content_length: 10 * 1024 * 1024)
expect(get_requests(client, :head_object).size).to be_zero
end
context 'when target and source objects are in different regions' do
let(:content_length) { 10 * 1024 * 1024 }
let(:source_region) { 'ap-southeast-1' }
let(:source_bucket) { 'source-bucket' }
let(:target_bucket) { 'target-bucket' }
let(:key) { 'my/source-key' }
let(:source_client) { S3::Client.new(stub_responses: true) }
let(:target_client) { S3::Client.new(stub_responses: true) }
let(:source_object) { S3::Object.new(bucket_name: source_bucket, key: key, client: source_client) }
let(:target_object) { S3::Object.new(bucket_name: target_bucket, key: key, client: target_client) }
let(:head_response) { Types::HeadObjectOutput.new(content_length: content_length) }
before do
source_client.stub_responses(
:head_object,
client.stub_data(:head_object, content_length: 10 * 1024 * 1024) # 10MB
)
end
context 'when the source is an S3::Object' do
it 'uses the content-length of the source object and region' do
target_object.copy_from(source_object, multipart_copy: true)
expect(get_requests(target_client, :head_object).size).to be_zero
expect(get_requests(source_client, :head_object).size).to eq(1)
expect(get_request_params(source_client, :head_object)).to(eq({ bucket: source_bucket, key: key }))
end
end
context 'when the source is a Hash' do
let(:source_hash) { { bucket: source_bucket, key: key } }
it 'uses :copy_source_client to query content_length' do
target_object.copy_from(source_hash,
multipart_copy: true, copy_source_client: source_client)
expect(get_requests(target_client, :head_object).size).to be_zero
expect(get_requests(source_client, :head_object).size).to eq(1)
expect(get_request_params(source_client, :head_object)).to(eq({ bucket: source_bucket, key: key }))
end
it 'uses :copy_source_region to construct a client' do
allow(S3::Client).to receive(:new).and_call_original
expect(S3::Client).to(
receive(:new).with(hash_including(region: source_region)).and_return(source_client)
)
target_object.copy_from(source_hash, multipart_copy: true, copy_source_region: source_region)
expect(get_requests(target_client, :head_object).size).to be_zero
expect(get_requests(source_client, :head_object).size).to eq(1)
expect(get_request_params(source_client, :head_object)).to(eq({ bucket: source_bucket, key: key }))
end
end
context 'when the source is a String' do
let(:source_string) { "#{source_bucket}/#{key}" }
it 'uses :copy_source_client to query content_length' do
target_object.copy_from(source_string, multipart_copy: true, copy_source_client: source_client)
expect(get_requests(target_client, :head_object).size).to be_zero
expect(get_requests(source_client, :head_object).size).to eq(1)
expect(get_request_params(source_client, :head_object)).to(eq({ bucket: source_bucket, key: key }))
end
it 'uses :copy_source_region to construct a client' do
allow(S3::Client).to receive(:new).and_call_original
expect(S3::Client).to(
receive(:new).with(hash_including(region: source_region)).and_return(source_client)
)
target_object.copy_from(source_string, multipart_copy: true, copy_source_region: source_region)
expect(get_requests(target_client, :head_object).size).to be_zero
expect(get_requests(source_client, :head_object).size).to eq(1)
expect(get_request_params(source_client, :head_object)).to(eq({ bucket: source_bucket, key: key }))
end
end
end
it 'does not modify given options' do
options = { multipart_copy: true }
object.copy_from('source-bucket/source%20key', options)
expect(options).to eq(multipart_copy: true)
end
end
context 'with multipart_copy: true and checksum_algorithm specified' do
before(:each) do
client.stub_responses(
:head_object,
client.stub_data(:head_object, content_length: 300 * 1024 * 1024) # 300MB
)
end
it 'includes the checksum algorithm when one is specified' do
object.copy_from('source-bucket/source%20key', multipart_copy: true, checksum_algorithm: 'SHA256')
expect(get_requests(client, :create_multipart_upload).size).to eq(1)
expect(get_request_params(client, :create_multipart_upload)).to(
include({ bucket: 'bucket', key: 'unescaped/key path', checksum_algorithm: 'SHA256' })
)
expect((requests = get_requests(client, :upload_part_copy).map { |req| req[:params] }).size).to eq(6)
requests.sort_by { |req| req[:part_number] }.each.with_index do |part, i|
n = i + 1
range = "bytes=#{(n - 1) * 52_428_800}-#{n * 52_428_800 - 1}"
expect(part).to eq({ bucket: 'bucket',
key: 'unescaped/key path',
part_number: n,
copy_source: 'source-bucket/source%20key',
copy_source_range: range,
upload_id: 'MultipartUploadId' })
end
expect(get_requests(client, :complete_multipart_upload).size).to eq(1)
expect(get_request_params(client, :complete_multipart_upload)).to(
include({ bucket: 'bucket',
key: 'unescaped/key path',
upload_id: 'MultipartUploadId',
multipart_upload: { parts: (1..6).map { |n| a_hash_including({ part_number: n }) } } })
)
end
end
context 'with multipart_copy: true and use_source_parts: true' do
before(:each) do
size = 300 * 1024 * 1024 # 300MB
part_size = size / 3 # 3 100MB parts
client.stub_responses(:head_object,
[client.stub_data(:head_object, content_length: size),
client.stub_data(:head_object, content_length: part_size, parts_count: 3),
client.stub_data(:head_object, content_length: part_size, parts_count: 3),
client.stub_data(:head_object, content_length: part_size, parts_count: 3),
client.stub_data(:head_object, content_length: part_size, parts_count: 3)])
end
it 'uses part sizes specified on the source' do
object.copy_from('source-bucket/source%20key', multipart_copy: true, use_source_parts: true)
expect(get_requests(client, :create_multipart_upload).size).to eq(1)
expect(get_request_params(client, :create_multipart_upload)).to(
include({ bucket: 'bucket', key: 'unescaped/key path' })
)
expect((requests = get_requests(client, :upload_part_copy).map { |req| req[:params] }).size).to eq(3)
requests.sort_by { |req| req[:part_number] }.each.with_index do |part, i|
n = i + 1
range = "bytes=#{(n - 1) * 104_857_600}-#{n * 104_857_600 - 1}"
expect(part).to eq({ bucket: 'bucket',
key: 'unescaped/key path',
part_number: n,
copy_source: 'source-bucket/source%20key',
copy_source_range: range,
upload_id: 'MultipartUploadId' })
end
expect(get_requests(client, :complete_multipart_upload).size).to eq(1)
expect(get_request_params(client, :complete_multipart_upload)).to(
include({ bucket: 'bucket',
key: 'unescaped/key path',
upload_id: 'MultipartUploadId',
multipart_upload: { parts: (1..3).map { |n| a_hash_including({ part_number: n }) } } })
)
end
end
end
end
end
end