Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/tests/functional/s3/test_sync_command.py
1567 views
1
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License"). You
4
# may not use this file except in compliance with the License. A copy of
5
# the License is located at
6
#
7
# http://aws.amazon.com/apache2.0/
8
#
9
# or in the "license" file accompanying this file. This file is
10
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
# ANY KIND, either express or implied. See the License for the specific
12
# language governing permissions and limitations under the License.
13
import os
14
15
from awscli.testutils import mock, cd
16
from awscli.compat import BytesIO
17
from tests.functional.s3 import BaseS3TransferCommandTest
18
19
20
class TestSyncCommand(BaseS3TransferCommandTest):
21
22
prefix = 's3 sync '
23
24
def test_website_redirect_ignore_paramfile(self):
25
full_path = self.files.create_file('foo.txt', 'mycontent')
26
cmdline = '%s %s s3://bucket/key.txt --website-redirect %s' % \
27
(self.prefix, self.files.rootdir, 'http://someserver')
28
self.parsed_responses = [
29
{"CommonPrefixes": [], "Contents": []},
30
{'ETag': '"c8afdb36c52cf4727836669019e69222"'}
31
]
32
self.run_cmd(cmdline, expected_rc=0)
33
34
# The only operations we should have called are ListObjectsV2/PutObject.
35
self.assertEqual(len(self.operations_called), 2, self.operations_called)
36
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
37
self.assertEqual(self.operations_called[1][0].name, 'PutObject')
38
# Make sure that the specified web address is used as opposed to the
39
# contents of the web address when uploading the object
40
self.assertEqual(
41
self.operations_called[1][1]['WebsiteRedirectLocation'],
42
'http://someserver'
43
)
44
45
def test_no_recursive_option(self):
46
cmdline = '. s3://mybucket --recursive'
47
# Return code will be 2 for invalid parameter ``--recursive``
48
self.run_cmd(cmdline, expected_rc=2)
49
50
def test_sync_from_non_existant_directory(self):
51
non_existant_directory = os.path.join(self.files.rootdir, 'fakedir')
52
cmdline = '%s %s s3://bucket/' % (self.prefix, non_existant_directory)
53
self.parsed_responses = [
54
{"CommonPrefixes": [], "Contents": []}
55
]
56
_, stderr, _ = self.run_cmd(cmdline, expected_rc=255)
57
self.assertIn('does not exist', stderr)
58
59
def test_sync_to_non_existant_directory(self):
60
key = 'foo.txt'
61
non_existant_directory = os.path.join(self.files.rootdir, 'fakedir')
62
cmdline = '%s s3://bucket/ %s' % (self.prefix, non_existant_directory)
63
self.parsed_responses = [
64
{"CommonPrefixes": [], "Contents": [
65
{"Key": key, "Size": 3,
66
"LastModified": "2014-01-09T20:45:49.000Z",
67
"ETag": '"c8afdb36c52cf4727836669019e69222-"',}]},
68
{'ETag': '"c8afdb36c52cf4727836669019e69222-"',
69
'Body': BytesIO(b'foo')}
70
]
71
self.run_cmd(cmdline, expected_rc=0)
72
# Make sure the file now exists.
73
self.assertTrue(
74
os.path.exists(os.path.join(non_existant_directory, key)))
75
76
def test_glacier_sync_with_force_glacier(self):
77
self.parsed_responses = [
78
{
79
'Contents': [
80
{'Key': 'foo/bar.txt', 'ContentLength': '100',
81
'LastModified': '00:00:00Z',
82
'StorageClass': 'GLACIER',
83
'Size': 100, 'ETag': '"foo-1"',},
84
],
85
'CommonPrefixes': []
86
},
87
{'ETag': '"foo-1"', 'Body': BytesIO(b'foo')},
88
]
89
cmdline = '%s s3://bucket/foo %s --force-glacier-transfer' % (
90
self.prefix, self.files.rootdir)
91
self.run_cmd(cmdline, expected_rc=0)
92
self.assertEqual(len(self.operations_called), 2, self.operations_called)
93
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
94
self.assertEqual(self.operations_called[1][0].name, 'GetObject')
95
96
def test_handles_glacier_incompatible_operations(self):
97
self.parsed_responses = [
98
{'Contents': [
99
{'Key': 'foo', 'Size': 100,
100
'LastModified': '00:00:00Z', 'StorageClass': 'GLACIER'},
101
{'Key': 'bar', 'Size': 100,
102
'LastModified': '00:00:00Z', 'StorageClass': 'DEEP_ARCHIVE'}
103
]}
104
]
105
cmdline = '%s s3://bucket/ %s' % (
106
self.prefix, self.files.rootdir)
107
_, stderr, _ = self.run_cmd(cmdline, expected_rc=2)
108
# There should not have been a download attempted because the
109
# operation was skipped because it is glacier and glacier
110
# deep archive incompatible.
111
self.assertEqual(len(self.operations_called), 1)
112
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
113
self.assertIn('GLACIER', stderr)
114
self.assertIn('s3://bucket/foo', stderr)
115
self.assertIn('s3://bucket/bar', stderr)
116
117
def test_turn_off_glacier_warnings(self):
118
self.parsed_responses = [
119
{'Contents': [
120
{'Key': 'foo', 'Size': 100,
121
'LastModified': '00:00:00Z', 'StorageClass': 'GLACIER'},
122
{'Key': 'bar', 'Size': 100,
123
'LastModified': '00:00:00Z', 'StorageClass': 'DEEP_ARCHIVE'}
124
]}
125
]
126
cmdline = '%s s3://bucket/ %s --ignore-glacier-warnings' % (
127
self.prefix, self.files.rootdir)
128
_, stderr, _ = self.run_cmd(cmdline, expected_rc=0)
129
# There should not have been a download attempted because the
130
# operation was skipped because it is glacier incompatible.
131
self.assertEqual(len(self.operations_called), 1)
132
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
133
self.assertEqual('', stderr)
134
135
def test_warning_on_invalid_timestamp(self):
136
full_path = self.files.create_file('foo.txt', 'mycontent')
137
138
cmdline = '%s %s s3://bucket/key.txt' % \
139
(self.prefix, self.files.rootdir)
140
self.parsed_responses = [
141
{"CommonPrefixes": [], "Contents": []},
142
{'ETag': '"c8afdb36c52cf4727836669019e69222"'}
143
]
144
# Patch get_file_stat to return a value indicating that an invalid
145
# timestamp was loaded. It is impossible to set an invalid timestamp
146
# on all OSes so it has to be patched.
147
# TODO: find another method to test this behavior without patching.
148
with mock.patch(
149
'awscli.customizations.s3.filegenerator.get_file_stat',
150
return_value=(None, None)
151
):
152
self.run_cmd(cmdline, expected_rc=2)
153
154
# We should still have put the object
155
self.assertEqual(len(self.operations_called), 2, self.operations_called)
156
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
157
self.assertEqual(self.operations_called[1][0].name, 'PutObject')
158
159
def test_sync_with_delete_on_downloads(self):
160
full_path = self.files.create_file('foo.txt', 'mycontent')
161
cmdline = '%s s3://bucket %s --delete' % (
162
self.prefix, self.files.rootdir)
163
self.parsed_responses = [
164
{"CommonPrefixes": [], "Contents": []},
165
{'ETag': '"c8afdb36c52cf4727836669019e69222"'}
166
]
167
self.run_cmd(cmdline, expected_rc=0)
168
169
# The only operations we should have called are ListObjectsV2.
170
self.assertEqual(len(self.operations_called), 1, self.operations_called)
171
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
172
173
self.assertFalse(os.path.exists(full_path))
174
175
# When a file has been deleted after listing,
176
# awscli.customizations.s3.utils.get_file_stat may raise either some kind
177
# of OSError, or a ValueError, depending on the environment. In both cases,
178
# the behaviour should be the same: skip the file and emit a warning.
179
#
180
# This test covers the case where a ValueError is emitted.
181
def test_sync_skips_over_files_deleted_between_listing_and_transfer_valueerror(self):
182
full_path = self.files.create_file('foo.txt', 'mycontent')
183
cmdline = '%s %s s3://bucket/' % (
184
self.prefix, self.files.rootdir)
185
186
# FileGenerator.list_files should skip over files that cause an
187
# IOError to be raised because they are missing when we try to
188
# get their stats. This IOError is translated to a ValueError in
189
# awscli.customizations.s3.utils.get_file_stat.
190
def side_effect(_):
191
os.remove(full_path)
192
raise ValueError()
193
with mock.patch(
194
'awscli.customizations.s3.filegenerator.get_file_stat',
195
side_effect=side_effect
196
):
197
self.run_cmd(cmdline, expected_rc=2)
198
199
# We should not call PutObject because the file was deleted
200
# before we could transfer it
201
self.assertEqual(len(self.operations_called), 1, self.operations_called)
202
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
203
204
# This test covers the case where an OSError is emitted.
205
def test_sync_skips_over_files_deleted_between_listing_and_transfer_oserror(self):
206
full_path = self.files.create_file('foo.txt', 'mycontent')
207
cmdline = '%s %s s3://bucket/' % (
208
self.prefix, self.files.rootdir)
209
210
# FileGenerator.list_files should skip over files that cause an
211
# OSError to be raised because they are missing when we try to
212
# get their stats.
213
def side_effect(_):
214
os.remove(full_path)
215
raise OSError()
216
with mock.patch(
217
'awscli.customizations.s3.filegenerator.get_file_stat',
218
side_effect=side_effect
219
):
220
self.run_cmd(cmdline, expected_rc=2)
221
222
# We should not call PutObject because the file was deleted
223
# before we could transfer it
224
self.assertEqual(len(self.operations_called), 1, self.operations_called)
225
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
226
227
def test_request_payer(self):
228
cmdline = '%s s3://sourcebucket/ s3://mybucket --request-payer' % (
229
self.prefix)
230
self.parsed_responses = [
231
# Response for ListObjects on source bucket
232
self.list_objects_response(['mykey']),
233
# Response for ListObjects on destination bucket
234
self.list_objects_response([]),
235
self.copy_object_response(),
236
]
237
self.run_cmd(cmdline, expected_rc=0)
238
self.assert_operations_called(
239
[
240
self.list_objects_request(
241
'sourcebucket', RequestPayer='requester'),
242
self.list_objects_request(
243
'mybucket', RequestPayer='requester'),
244
self.copy_object_request(
245
'sourcebucket', 'mykey', 'mybucket', 'mykey',
246
RequestPayer='requester')
247
]
248
)
249
250
def test_request_payer_with_deletes(self):
251
cmdline = '%s s3://sourcebucket/ s3://mybucket' % self.prefix
252
cmdline += ' --request-payer'
253
cmdline += ' --delete'
254
self.parsed_responses = [
255
# Response for ListObjects on source bucket
256
self.list_objects_response([]),
257
# Response for ListObjects on destination bucket
258
self.list_objects_response(['key-to-delete']),
259
self.delete_object_response()
260
]
261
self.run_cmd(cmdline, expected_rc=0)
262
self.assert_operations_called(
263
[
264
self.list_objects_request(
265
'sourcebucket', RequestPayer='requester'),
266
self.list_objects_request(
267
'mybucket', RequestPayer='requester'),
268
self.delete_object_request(
269
'mybucket', 'key-to-delete', RequestPayer='requester'),
270
]
271
)
272
273
def test_with_accesspoint_arn(self):
274
accesspoint_arn = (
275
'arn:aws:s3:us-west-2:123456789012:accesspoint/endpoint'
276
)
277
cmdline = self.prefix
278
cmdline += 's3://%s' % accesspoint_arn
279
cmdline += ' %s' % self.files.rootdir
280
self.parsed_responses = [
281
self.list_objects_response(['mykey']),
282
self.get_object_response(),
283
]
284
self.run_cmd(cmdline, expected_rc=0)
285
self.assert_operations_called(
286
[
287
self.list_objects_request(accesspoint_arn),
288
self.get_object_request(accesspoint_arn, 'mykey')
289
]
290
)
291
292
def test_upload_with_checksum_algorithm_sha1(self):
293
self.files.create_file('foo.txt', 'contents')
294
cmdline = f'{self.prefix} {self.files.rootdir} s3://bucket/ --checksum-algorithm SHA1'
295
self.run_cmd(cmdline, expected_rc=0)
296
self.assertEqual(self.operations_called[1][0].name, 'PutObject')
297
self.assertEqual(self.operations_called[1][1]['ChecksumAlgorithm'], 'SHA1')
298
299
def test_copy_with_checksum_algorithm_update_sha1(self):
300
cmdline = f'{self.prefix} s3://src-bucket/ s3://dest-bucket/ --checksum-algorithm SHA1'
301
self.parsed_responses = [
302
# Response for ListObjects on source bucket
303
{
304
'Contents': [
305
{
306
'Key': 'mykey',
307
'LastModified': '00:00:00Z',
308
'Size': 100,
309
'ChecksumAlgorithm': 'SHA1'
310
}
311
],
312
'CommonPrefixes': []
313
},
314
# Response for ListObjects on destination bucket
315
self.list_objects_response([]),
316
# Response for CopyObject
317
{
318
'ChecksumSHA1': 'sha1-checksum'
319
}
320
]
321
self.run_cmd(cmdline, expected_rc=0)
322
self.assert_operations_called(
323
[
324
self.list_objects_request('src-bucket'),
325
self.list_objects_request('dest-bucket'),
326
(
327
'CopyObject', {
328
'CopySource': {
329
'Bucket': 'src-bucket',
330
'Key': 'mykey'
331
},
332
'Bucket': 'dest-bucket',
333
'Key': 'mykey',
334
'ChecksumAlgorithm': 'SHA1'
335
}
336
)
337
]
338
)
339
340
def test_upload_with_checksum_algorithm_sha256(self):
341
self.files.create_file('foo.txt', 'contents')
342
cmdline = f'{self.prefix} {self.files.rootdir} s3://bucket/ --checksum-algorithm SHA256'
343
self.run_cmd(cmdline, expected_rc=0)
344
self.assertEqual(self.operations_called[1][0].name, 'PutObject')
345
self.assertEqual(self.operations_called[1][1]['ChecksumAlgorithm'], 'SHA256')
346
347
def test_download_with_checksum_mode_sha1(self):
348
self.parsed_responses = [
349
self.list_objects_response(['bucket']),
350
# Mocked GetObject response with a checksum algorithm specified
351
{
352
'ETag': 'foo-1',
353
'ChecksumSHA1': 'checksum',
354
'Body': BytesIO(b'foo')
355
}
356
]
357
cmdline = f'{self.prefix} s3://bucket/foo {self.files.rootdir} --checksum-mode ENABLED'
358
self.run_cmd(cmdline, expected_rc=0)
359
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
360
self.assertEqual(self.operations_called[1][0].name, 'GetObject')
361
self.assertIn(('ChecksumMode', 'ENABLED'), self.operations_called[1][1].items())
362
363
def test_download_with_checksum_mode_sha256(self):
364
self.parsed_responses = [
365
self.list_objects_response(['bucket']),
366
# Mocked GetObject response with a checksum algorithm specified
367
{
368
'ETag': 'foo-1',
369
'ChecksumSHA256': 'checksum',
370
'Body': BytesIO(b'foo')
371
}
372
]
373
cmdline = f'{self.prefix} s3://bucket/foo {self.files.rootdir} --checksum-mode ENABLED'
374
self.run_cmd(cmdline, expected_rc=0)
375
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
376
self.assertEqual(self.operations_called[1][0].name, 'GetObject')
377
self.assertIn(('ChecksumMode', 'ENABLED'), self.operations_called[1][1].items())
378
379
def test_download_with_checksum_mode_crc64nvme(self):
380
self.parsed_responses = [
381
self.list_objects_response(['bucket']),
382
# Mocked GetObject response with a checksum algorithm specified
383
{
384
'ETag': 'foo-1',
385
'ChecksumCRC64NVME': 'checksum',
386
'Body': BytesIO(b'foo')
387
}
388
]
389
cmdline = f'{self.prefix} s3://bucket/foo {self.files.rootdir} --checksum-mode ENABLED'
390
self.run_cmd(cmdline, expected_rc=0)
391
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
392
self.assertEqual(self.operations_called[1][0].name, 'GetObject')
393
self.assertIn(('ChecksumMode', 'ENABLED'), self.operations_called[1][1].items())
394
395
396
class TestSyncCommandWithS3Express(BaseS3TransferCommandTest):
397
398
prefix = 's3 sync '
399
400
def test_incompatible_with_sync_upload(self):
401
cmdline = '%s %s s3://testdirectorybucket--usw2-az1--x-s3/' % (self.prefix, self.files.rootdir)
402
stderr = self.run_cmd(cmdline, expected_rc=255)[1]
403
self.assertIn('Cannot use sync command with a directory bucket.', stderr)
404
405
def test_incompatible_with_sync_download(self):
406
cmdline = '%s s3://testdirectorybucket--usw2-az1--x-s3/ %s' % (self.prefix, self.files.rootdir)
407
stderr = self.run_cmd(cmdline, expected_rc=255)[1]
408
self.assertIn('Cannot use sync command with a directory bucket.', stderr)
409
410
def test_incompatible_with_sync_copy(self):
411
cmdline = '%s s3://bucket/ s3://testdirectorybucket--usw2-az1--x-s3/' % self.prefix
412
stderr = self.run_cmd(cmdline, expected_rc=255)[1]
413
self.assertIn('Cannot use sync command with a directory bucket.', stderr)
414
415
def test_incompatible_with_sync_with_delete(self):
416
cmdline = '%s s3://bucket/ s3://testdirectorybucket--usw2-az1--x-s3/ --delete' % self.prefix
417
stderr = self.run_cmd(cmdline, expected_rc=255)[1]
418
self.assertIn('Cannot use sync command with a directory bucket.', stderr)
419
420
def test_compatible_with_sync_with_local_directory_like_directory_bucket(self):
421
self.parsed_responses = [
422
{'Contents': []}
423
]
424
425
cmdline = '%s s3://bucket/ testdirectorybucket--usw2-az1--x-s3/' % self.prefix
426
with cd(self.files.rootdir):
427
_, stderr, _ = self.run_cmd(cmdline)
428
429
# Just asserting that command validated and made an API call
430
self.assertEqual(len(self.operations_called), 1)
431
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
432
433