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
2621 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 cd, mock, skip_if_case_sensitive, skip_if_windows
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
'ETag': 'foo'
311
}
312
],
313
'CommonPrefixes': []
314
},
315
# Response for ListObjects on destination bucket
316
self.list_objects_response([]),
317
# Response for CopyObject
318
{
319
'ChecksumSHA1': 'sha1-checksum'
320
}
321
]
322
self.run_cmd(cmdline, expected_rc=0)
323
self.assert_operations_called(
324
[
325
self.list_objects_request('src-bucket'),
326
self.list_objects_request('dest-bucket'),
327
(
328
'CopyObject', {
329
'CopySource': {
330
'Bucket': 'src-bucket',
331
'Key': 'mykey'
332
},
333
'Bucket': 'dest-bucket',
334
'Key': 'mykey',
335
'ChecksumAlgorithm': 'SHA1'
336
}
337
)
338
]
339
)
340
341
def test_upload_with_checksum_algorithm_sha256(self):
342
self.files.create_file('foo.txt', 'contents')
343
cmdline = f'{self.prefix} {self.files.rootdir} s3://bucket/ --checksum-algorithm SHA256'
344
self.run_cmd(cmdline, expected_rc=0)
345
self.assertEqual(self.operations_called[1][0].name, 'PutObject')
346
self.assertEqual(self.operations_called[1][1]['ChecksumAlgorithm'], 'SHA256')
347
348
def test_download_with_checksum_mode_sha1(self):
349
self.parsed_responses = [
350
self.list_objects_response(['bucket']),
351
# Mocked GetObject response with a checksum algorithm specified
352
{
353
'ETag': 'foo-1',
354
'ChecksumSHA1': 'checksum',
355
'Body': BytesIO(b'foo')
356
}
357
]
358
cmdline = f'{self.prefix} s3://bucket/foo {self.files.rootdir} --checksum-mode ENABLED'
359
self.run_cmd(cmdline, expected_rc=0)
360
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
361
self.assertEqual(self.operations_called[1][0].name, 'GetObject')
362
self.assertIn(('ChecksumMode', 'ENABLED'), self.operations_called[1][1].items())
363
364
def test_download_with_checksum_mode_sha256(self):
365
self.parsed_responses = [
366
self.list_objects_response(['bucket']),
367
# Mocked GetObject response with a checksum algorithm specified
368
{
369
'ETag': 'foo-1',
370
'ChecksumSHA256': 'checksum',
371
'Body': BytesIO(b'foo')
372
}
373
]
374
cmdline = f'{self.prefix} s3://bucket/foo {self.files.rootdir} --checksum-mode ENABLED'
375
self.run_cmd(cmdline, expected_rc=0)
376
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
377
self.assertEqual(self.operations_called[1][0].name, 'GetObject')
378
self.assertIn(('ChecksumMode', 'ENABLED'), self.operations_called[1][1].items())
379
380
def test_download_with_checksum_mode_crc64nvme(self):
381
self.parsed_responses = [
382
self.list_objects_response(['bucket']),
383
# Mocked GetObject response with a checksum algorithm specified
384
{
385
'ETag': 'foo-1',
386
'ChecksumCRC64NVME': 'checksum',
387
'Body': BytesIO(b'foo')
388
}
389
]
390
cmdline = f'{self.prefix} s3://bucket/foo {self.files.rootdir} --checksum-mode ENABLED'
391
self.run_cmd(cmdline, expected_rc=0)
392
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
393
self.assertEqual(self.operations_called[1][0].name, 'GetObject')
394
self.assertIn(('ChecksumMode', 'ENABLED'), self.operations_called[1][1].items())
395
396
397
class TestSyncCommandWithS3Express(BaseS3TransferCommandTest):
398
399
prefix = 's3 sync '
400
401
def test_incompatible_with_sync_upload(self):
402
cmdline = '%s %s s3://testdirectorybucket--usw2-az1--x-s3/' % (self.prefix, self.files.rootdir)
403
stderr = self.run_cmd(cmdline, expected_rc=255)[1]
404
self.assertIn('Cannot use sync command with a directory bucket.', stderr)
405
406
def test_incompatible_with_sync_download(self):
407
cmdline = '%s s3://testdirectorybucket--usw2-az1--x-s3/ %s' % (self.prefix, self.files.rootdir)
408
stderr = self.run_cmd(cmdline, expected_rc=255)[1]
409
self.assertIn('Cannot use sync command with a directory bucket.', stderr)
410
411
def test_incompatible_with_sync_copy(self):
412
cmdline = '%s s3://bucket/ s3://testdirectorybucket--usw2-az1--x-s3/' % self.prefix
413
stderr = self.run_cmd(cmdline, expected_rc=255)[1]
414
self.assertIn('Cannot use sync command with a directory bucket.', stderr)
415
416
def test_incompatible_with_sync_with_delete(self):
417
cmdline = '%s s3://bucket/ s3://testdirectorybucket--usw2-az1--x-s3/ --delete' % self.prefix
418
stderr = self.run_cmd(cmdline, expected_rc=255)[1]
419
self.assertIn('Cannot use sync command with a directory bucket.', stderr)
420
421
def test_compatible_with_sync_with_local_directory_like_directory_bucket(self):
422
self.parsed_responses = [
423
{'Contents': []}
424
]
425
426
cmdline = '%s s3://bucket/ testdirectorybucket--usw2-az1--x-s3/' % self.prefix
427
with cd(self.files.rootdir):
428
_, stderr, _ = self.run_cmd(cmdline)
429
430
# Just asserting that command validated and made an API call
431
self.assertEqual(len(self.operations_called), 1)
432
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
433
434
435
class TestSyncCaseConflict(BaseS3TransferCommandTest):
436
prefix = 's3 sync '
437
438
def setUp(self):
439
super().setUp()
440
self.lower_key = 'a.txt'
441
self.upper_key = 'A.txt'
442
443
@skip_if_case_sensitive()
444
def test_error_with_existing_file(self):
445
self.files.create_file(self.lower_key, 'mycontent')
446
cmd = (
447
f"{self.prefix} s3://bucket {self.files.rootdir} "
448
"--case-conflict error"
449
)
450
self.parsed_responses = [self.list_objects_response([self.upper_key])]
451
_, stderr, _ = self.run_cmd(cmd, expected_rc=1)
452
assert f"Failed to download bucket/{self.upper_key}" in stderr
453
454
def test_error_with_case_conflicts_in_s3(self):
455
cmd = (
456
f"{self.prefix} s3://bucket {self.files.rootdir} "
457
"--case-conflict error"
458
)
459
self.parsed_responses = [
460
self.list_objects_response([self.upper_key, self.lower_key])
461
]
462
_, stderr, _ = self.run_cmd(cmd, expected_rc=1)
463
assert f"Failed to download bucket/{self.lower_key}" in stderr
464
465
@skip_if_case_sensitive()
466
def test_warn_with_existing_file(self):
467
self.files.create_file(self.lower_key, 'mycontent')
468
cmd = (
469
f"{self.prefix} s3://bucket {self.files.rootdir} "
470
"--case-conflict warn"
471
)
472
self.parsed_responses = [
473
self.list_objects_response([self.upper_key]),
474
self.get_object_response(),
475
]
476
_, stderr, _ = self.run_cmd(cmd, expected_rc=0)
477
assert f"warning: Downloading bucket/{self.upper_key}" in stderr
478
479
@skip_if_windows("Can't rename to same file")
480
def test_warn_with_case_conflicts_in_s3(self):
481
cmd = (
482
f"{self.prefix} s3://bucket {self.files.rootdir} "
483
"--case-conflict warn"
484
)
485
self.parsed_responses = [
486
self.list_objects_response([self.upper_key, self.lower_key]),
487
self.get_object_response(),
488
self.get_object_response(),
489
]
490
_, stderr, _ = self.run_cmd(cmd, expected_rc=0)
491
assert f"warning: Downloading bucket/{self.lower_key}" in stderr
492
493
@skip_if_case_sensitive()
494
def test_skip_with_existing_file(self):
495
self.files.create_file(self.lower_key, 'mycontent')
496
cmd = (
497
f"{self.prefix} s3://bucket {self.files.rootdir} "
498
"--case-conflict skip"
499
)
500
self.parsed_responses = [self.list_objects_response([self.upper_key])]
501
_, stderr, _ = self.run_cmd(cmd, expected_rc=0)
502
assert f"warning: Skipping bucket/{self.upper_key}" in stderr
503
504
def test_skip_with_case_conflicts_in_s3(self):
505
cmd = (
506
f"{self.prefix} s3://bucket {self.files.rootdir} "
507
"--case-conflict skip"
508
)
509
self.parsed_responses = [
510
self.list_objects_response([self.upper_key, self.lower_key]),
511
self.get_object_response(),
512
]
513
_, stderr, _ = self.run_cmd(cmd, expected_rc=0)
514
assert f"warning: Skipping bucket/{self.lower_key}" in stderr
515
516
def test_ignore_with_existing_file(self):
517
self.files.create_file(self.lower_key, 'mycontent')
518
cmd = (
519
f"{self.prefix} s3://bucket {self.files.rootdir} "
520
"--case-conflict ignore"
521
)
522
self.parsed_responses = [
523
self.list_objects_response([self.upper_key]),
524
self.get_object_response(),
525
]
526
self.run_cmd(cmd, expected_rc=0)
527
528
@skip_if_windows("Can't rename to same file")
529
def test_ignore_with_case_conflicts_in_s3(self):
530
cmd = (
531
f"{self.prefix} s3://bucket {self.files.rootdir} "
532
"--case-conflict ignore"
533
)
534
self.parsed_responses = [
535
self.list_objects_response([self.upper_key, self.lower_key]),
536
self.get_object_response(),
537
self.get_object_response(),
538
]
539
self.run_cmd(cmd, expected_rc=0)
540
541