Skip to content

Commit 68fd8f6

Browse files
Abort multi part download if the object is modified during download (#3248)
Abort multi part download if the object is modified during download
1 parent 9325927 commit 68fd8f6

File tree

3 files changed

+63
-10
lines changed

3 files changed

+63
-10
lines changed

gems/aws-sdk-s3/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
Unreleased Changes
22
------------------
3+
* Issue - Abort multipart download if object is modified during download.
34

45
1.186.0 (2025-05-12)
56
------------------

gems/aws-sdk-s3/lib/aws-sdk-s3/file_downloader.rb

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def download(destination, options = {})
4343
when 'get_range'
4444
if @chunk_size
4545
resp = @client.head_object(@params)
46-
multithreaded_get_by_ranges(resp.content_length)
46+
multithreaded_get_by_ranges(resp.content_length, resp.etag)
4747
else
4848
msg = 'In :get_range mode, :chunk_size must be provided'
4949
raise ArgumentError, msg
@@ -71,26 +71,26 @@ def multipart_download
7171
if resp.content_length <= MIN_CHUNK_SIZE
7272
single_request
7373
else
74-
multithreaded_get_by_ranges(resp.content_length)
74+
multithreaded_get_by_ranges(resp.content_length, resp.etag)
7575
end
7676
else
7777
# partNumber is an option
7878
resp = @client.head_object(@params)
7979
if resp.content_length <= MIN_CHUNK_SIZE
8080
single_request
8181
else
82-
compute_mode(resp.content_length, count)
82+
compute_mode(resp.content_length, count, resp.etag)
8383
end
8484
end
8585
end
8686

87-
def compute_mode(file_size, count)
87+
def compute_mode(file_size, count, etag)
8888
chunk_size = compute_chunk(file_size)
8989
part_size = (file_size.to_f / count.to_f).ceil
9090
if chunk_size < part_size
91-
multithreaded_get_by_ranges(file_size)
91+
multithreaded_get_by_ranges(file_size, etag)
9292
else
93-
multithreaded_get_by_parts(count, file_size)
93+
multithreaded_get_by_parts(count, file_size, etag)
9494
end
9595
end
9696

@@ -122,7 +122,7 @@ def batches(chunks, mode)
122122
chunks.each_slice(@thread_count).to_a
123123
end
124124

125-
def multithreaded_get_by_ranges(file_size)
125+
def multithreaded_get_by_ranges(file_size, etag)
126126
offset = 0
127127
default_chunk_size = compute_chunk(file_size)
128128
chunks = []
@@ -134,17 +134,17 @@ def multithreaded_get_by_ranges(file_size)
134134
chunks << Part.new(
135135
part_number: part_number,
136136
size: (progress-offset),
137-
params: @params.merge(range: range)
137+
params: @params.merge(range: range, if_match: etag)
138138
)
139139
part_number += 1
140140
offset = progress
141141
end
142142
download_in_threads(PartList.new(chunks), file_size)
143143
end
144144

145-
def multithreaded_get_by_parts(n_parts, total_size)
145+
def multithreaded_get_by_parts(n_parts, total_size, etag)
146146
parts = (1..n_parts).map do |part|
147-
Part.new(part_number: part, params: @params.merge(part_number: part))
147+
Part.new(part_number: part, params: @params.merge(part_number: part, if_match: etag))
148148
end
149149
download_in_threads(PartList.new(parts), total_size)
150150
end

gems/aws-sdk-s3/spec/object/download_file_spec.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,58 @@ module S3
221221
end.to raise_error(Aws::Errors::ChecksumError)
222222
end
223223

224+
it 'does not download object when ETAG does not match during multipart get by ranges' do
225+
allow(client).to receive(:head_object).with({
226+
bucket: 'bucket',
227+
key: 'single',
228+
part_number: 1,
229+
}).and_return(
230+
client.stub_data(
231+
:head_object,
232+
content_length: 15 * one_meg,
233+
parts_count: nil,
234+
etag: 'test-etag'
235+
)
236+
)
237+
238+
client.stub_responses(:get_object, -> (ctx) {
239+
expect(ctx.params[:if_match]).to eq('test-etag')
240+
'PreconditionFailed'
241+
})
242+
243+
thread = double(value: nil)
244+
expect(Thread).to receive(:new).and_yield.and_return(thread)
245+
246+
expect do
247+
single_obj.download_file(path)
248+
end.to raise_error(Aws::S3::Errors::PreconditionFailed)
249+
end
250+
251+
it 'does not download object when ETAG does not match during multipart get by parts' do
252+
allow(client).to receive(:head_object).with({
253+
bucket: 'bucket',
254+
key: 'large'
255+
}).and_return(
256+
client.stub_data(
257+
:head_object,
258+
content_length: 20 * one_meg,
259+
etag: 'test-etag'
260+
)
261+
)
262+
263+
client.stub_responses(:get_object, -> (ctx) {
264+
expect(ctx.params[:if_match]).to eq('test-etag')
265+
'PreconditionFailed'
266+
})
267+
268+
thread = double(value: nil)
269+
expect(Thread).to receive(:new).and_yield.and_return(thread)
270+
271+
expect do
272+
large_obj.download_file(path)
273+
end.to raise_error(Aws::S3::Errors::PreconditionFailed)
274+
end
275+
224276
it 'calls on_checksum_validated on single part' do
225277
callback_data = {called: 0}
226278
mutex = Mutex.new

0 commit comments

Comments
 (0)