Path: blob/develop/tests/unit/customizations/s3/test_filegenerator.py
1569 views
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.1#2# Licensed under the Apache License, Version 2.0 (the "License"). You3# may not use this file except in compliance with the License. A copy of4# the License is located at5#6# http://aws.amazon.com/apache2.0/7#8# or in the "license" file accompanying this file. This file is9# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF10# ANY KIND, either express or implied. See the License for the specific11# language governing permissions and limitations under the License.12import os13import platform14from awscli.testutils import mock, unittest, FileCreator, BaseAWSCommandParamsTest15from awscli.testutils import skip_if_windows16import stat17import tempfile18import shutil19import socket2021from botocore.exceptions import ClientError2223from awscli.customizations.s3.filegenerator import FileGenerator, \24FileDecodingError, FileStat, is_special_file, is_readable25from awscli.customizations.s3.utils import get_file_stat, EPOCH_TIME26from tests.unit.customizations.s3 import make_loc_files, clean_loc_files, \27compare_files282930@skip_if_windows('Special files only supported on mac/linux')31class TestIsSpecialFile(unittest.TestCase):32def setUp(self):33self.files = FileCreator()34self.filename = 'foo'3536def tearDown(self):37self.files.remove_all()3839def test_is_character_device(self):40file_path = os.path.join(self.files.rootdir, self.filename)41self.files.create_file(self.filename, contents='')42with mock.patch('stat.S_ISCHR') as mock_class:43mock_class.return_value = True44self.assertTrue(is_special_file(file_path))4546def test_is_block_device(self):47file_path = os.path.join(self.files.rootdir, self.filename)48self.files.create_file(self.filename, contents='')49with mock.patch('stat.S_ISBLK') as mock_class:50mock_class.return_value = True51self.assertTrue(is_special_file(file_path))5253def test_is_fifo(self):54file_path = os.path.join(self.files.rootdir, self.filename)55mode = 0o600 | stat.S_IFIFO56os.mknod(file_path, mode)57self.assertTrue(is_special_file(file_path))5859def test_is_socket(self):60file_path = os.path.join(self.files.rootdir, self.filename)61sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)62sock.bind(file_path)63self.assertTrue(is_special_file(file_path))646566class TestIsReadable(unittest.TestCase):67def setUp(self):68self.files = FileCreator()69self.filename = 'foo'70self.full_path = os.path.join(self.files.rootdir, self.filename)7172def tearDown(self):73self.files.remove_all()7475def test_unreadable_file(self):76self.files.create_file(self.filename, contents="foo")77open_function = 'awscli.customizations.s3.filegenerator._open'78with mock.patch(open_function) as mock_class:79mock_class.side_effect = OSError()80self.assertFalse(is_readable(self.full_path))8182def test_unreadable_directory(self):83os.mkdir(self.full_path)84with mock.patch('os.listdir') as mock_class:85mock_class.side_effect = OSError()86self.assertFalse(is_readable(self.full_path))878889class LocalFileGeneratorTest(unittest.TestCase):90def setUp(self):91self.client = None92self.file_creator = FileCreator()93self.files = make_loc_files(self.file_creator)94self.local_file = self.files[0]95self.local_dir = self.files[3] + os.sep9697def tearDown(self):98clean_loc_files(self.file_creator)99100def test_local_file(self):101"""102Generate a single local file.103"""104input_local_file = {'src': {'path': self.local_file,105'type': 'local'},106'dest': {'path': 'bucket/text1.txt',107'type': 's3'},108'dir_op': False, 'use_src_name': False}109params = {'region': 'us-east-1'}110files = FileGenerator(self.client, '').call(input_local_file)111result_list = []112for filename in files:113result_list.append(filename)114size, last_update = get_file_stat(self.local_file)115file_stat = FileStat(src=self.local_file, dest='bucket/text1.txt',116compare_key='text1.txt', size=size,117last_update=last_update, src_type='local',118dest_type='s3', operation_name='')119ref_list = [file_stat]120self.assertEqual(len(result_list), len(ref_list))121for i in range(len(result_list)):122compare_files(self, result_list[i], ref_list[i])123124def test_local_directory(self):125"""126Generate an entire local directory.127"""128input_local_dir = {'src': {'path': self.local_dir,129'type': 'local'},130'dest': {'path': 'bucket/',131'type': 's3'},132'dir_op': True, 'use_src_name': True}133params = {'region': 'us-east-1'}134files = FileGenerator(self.client, '').call(input_local_dir)135result_list = []136for filename in files:137result_list.append(filename)138size, last_update = get_file_stat(self.local_file)139file_stat = FileStat(src=self.local_file, dest='bucket/text1.txt',140compare_key='text1.txt', size=size,141last_update=last_update, src_type='local',142dest_type='s3', operation_name='')143path = self.local_dir + 'another_directory' + os.sep \144+ 'text2.txt'145size, last_update = get_file_stat(path)146file_stat2 = FileStat(src=path,147dest='bucket/another_directory/text2.txt',148compare_key='another_directory/text2.txt',149size=size, last_update=last_update,150src_type='local',151dest_type='s3', operation_name='')152ref_list = [file_stat2, file_stat]153self.assertEqual(len(result_list), len(ref_list))154for i in range(len(result_list)):155compare_files(self, result_list[i], ref_list[i])156157158@skip_if_windows('Symlink tests only supported on mac/linux')159class TestIgnoreFilesLocally(unittest.TestCase):160"""161This class tests the ability to ignore particular files. This includes162skipping symlink when desired.163"""164def setUp(self):165self.client = None166self.files = FileCreator()167168def tearDown(self):169self.files.remove_all()170171def test_warning(self):172path = os.path.join(self.files.rootdir, 'badsymlink')173os.symlink('non-existent-file', path)174filegenerator = FileGenerator(self.client, '', True)175self.assertTrue(filegenerator.should_ignore_file(path))176177def test_skip_symlink(self):178filename = 'foo.txt'179self.files.create_file(os.path.join(self.files.rootdir,180filename),181contents='foo.txt contents')182sym_path = os.path.join(self.files.rootdir, 'symlink')183os.symlink(filename, sym_path)184filegenerator = FileGenerator(self.client, '', False)185self.assertTrue(filegenerator.should_ignore_file(sym_path))186187def test_no_skip_symlink(self):188filename = 'foo.txt'189path = self.files.create_file(os.path.join(self.files.rootdir,190filename),191contents='foo.txt contents')192sym_path = os.path.join(self.files.rootdir, 'symlink')193os.symlink(path, sym_path)194filegenerator = FileGenerator(self.client, '', True)195self.assertFalse(filegenerator.should_ignore_file(sym_path))196self.assertFalse(filegenerator.should_ignore_file(path))197198def test_no_skip_symlink_dir(self):199filename = 'dir'200path = os.path.join(self.files.rootdir, 'dir/')201os.mkdir(path)202sym_path = os.path.join(self.files.rootdir, 'symlink')203os.symlink(path, sym_path)204filegenerator = FileGenerator(self.client, '', True)205self.assertFalse(filegenerator.should_ignore_file(sym_path))206self.assertFalse(filegenerator.should_ignore_file(path))207208209class TestThrowsWarning(unittest.TestCase):210def setUp(self):211self.files = FileCreator()212self.root = self.files.rootdir213self.client = None214215def tearDown(self):216self.files.remove_all()217218def test_no_warning(self):219file_gen = FileGenerator(self.client, '', False)220self.files.create_file("foo.txt", contents="foo")221full_path = os.path.join(self.root, "foo.txt")222return_val = file_gen.triggers_warning(full_path)223self.assertFalse(return_val)224self.assertTrue(file_gen.result_queue.empty())225226def test_no_exists(self):227file_gen = FileGenerator(self.client, '', False)228filename = os.path.join(self.root, 'file')229return_val = file_gen.triggers_warning(filename)230self.assertTrue(return_val)231warning_message = file_gen.result_queue.get()232self.assertEqual(warning_message.message,233("warning: Skipping file %s. File does not exist." %234filename))235236def test_no_read_access(self):237file_gen = FileGenerator(self.client, '', False)238self.files.create_file("foo.txt", contents="foo")239full_path = os.path.join(self.root, "foo.txt")240open_function = 'awscli.customizations.s3.filegenerator._open'241with mock.patch(open_function) as mock_class:242mock_class.side_effect = OSError()243return_val = file_gen.triggers_warning(full_path)244self.assertTrue(return_val)245warning_message = file_gen.result_queue.get()246self.assertEqual(warning_message.message,247("warning: Skipping file %s. File/Directory is "248"not readable." % full_path))249250@skip_if_windows('Special files only supported on mac/linux')251def test_is_special_file_warning(self):252file_gen = FileGenerator(self.client, '', False)253file_path = os.path.join(self.files.rootdir, 'foo')254# Use socket for special file.255sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)256sock.bind(file_path)257return_val = file_gen.triggers_warning(file_path)258self.assertTrue(return_val)259warning_message = file_gen.result_queue.get()260self.assertEqual(warning_message.message,261("warning: Skipping file %s. File is character "262"special device, block special device, FIFO, or "263"socket." % file_path))264265266@skip_if_windows('Symlink tests only supported on mac/linux')267class TestSymlinksIgnoreFiles(unittest.TestCase):268"""269This class tests the ability to list out the correct local files270depending on if symlinks are being followed. Also tests to ensure271broken symlinks fail.272"""273def setUp(self):274self.client = None275self.files = FileCreator()276# List of local filenames.277self.filenames = []278self.root = self.files.rootdir279self.bucket = 'bucket/'280filename_1 = self.files.create_file('foo.txt',281contents='foo.txt contents')282self.filenames.append(filename_1)283nested_dir = os.path.join(self.root, 'realfiles')284os.mkdir(nested_dir)285filename_2 = self.files.create_file(os.path.join(nested_dir,286'bar.txt'),287contents='bar.txt contents')288self.filenames.append(filename_2)289# Names of symlinks.290self.symlinks = []291# Names of files if symlinks are followed.292self.symlink_files = []293# Create symlink to file foo.txt.294symlink_1 = os.path.join(self.root, 'symlink_1')295os.symlink(filename_1, symlink_1)296self.symlinks.append(symlink_1)297self.symlink_files.append(symlink_1)298# Create a symlink to a file that does not exist.299symlink_2 = os.path.join(self.root, 'symlink_2')300os.symlink('non-existent-file', symlink_2)301self.symlinks.append(symlink_2)302# Create a symlink to directory realfiles303symlink_3 = os.path.join(self.root, 'symlink_3')304os.symlink(nested_dir, symlink_3)305self.symlinks.append(symlink_3)306self.symlink_files.append(os.path.join(symlink_3, 'bar.txt'))307308def tearDown(self):309self.files.remove_all()310311def test_no_follow_symlink(self):312abs_root = str(os.path.abspath(self.root) + os.sep)313input_local_dir = {'src': {'path': abs_root,314'type': 'local'},315'dest': {'path': self.bucket,316'type': 's3'},317'dir_op': True, 'use_src_name': True}318file_stats = FileGenerator(self.client, '', False).call(input_local_dir)319self.filenames.sort()320result_list = []321for file_stat in file_stats:322result_list.append(getattr(file_stat, 'src'))323self.assertEqual(len(result_list), len(self.filenames))324# Just check to make sure the right local files are generated.325for i in range(len(result_list)):326filename = str(os.path.abspath(self.filenames[i]))327self.assertEqual(result_list[i], filename)328329def test_warn_bad_symlink(self):330"""331This tests to make sure it fails when following bad symlinks.332"""333abs_root = str(os.path.abspath(self.root) + os.sep)334input_local_dir = {'src': {'path': abs_root,335'type': 'local'},336'dest': {'path': self.bucket,337'type': 's3'},338'dir_op': True, 'use_src_name': True}339file_stats = FileGenerator(self.client, '', True).call(input_local_dir)340file_gen = FileGenerator(self.client, '', True)341file_stats = file_gen.call(input_local_dir)342all_filenames = self.filenames + self.symlink_files343all_filenames.sort()344result_list = []345for file_stat in file_stats:346result_list.append(getattr(file_stat, 'src'))347self.assertEqual(len(result_list), len(all_filenames))348# Just check to make sure the right local files are generated.349for i in range(len(result_list)):350filename = str(os.path.abspath(all_filenames[i]))351self.assertEqual(result_list[i], filename)352self.assertFalse(file_gen.result_queue.empty())353354def test_follow_symlink(self):355# First remove the bad symlink.356os.remove(os.path.join(self.root, 'symlink_2'))357abs_root = str(os.path.abspath(self.root) + os.sep)358input_local_dir = {'src': {'path': abs_root,359'type': 'local'},360'dest': {'path': self.bucket,361'type': 's3'},362'dir_op': True, 'use_src_name': True}363file_stats = FileGenerator(self.client, '', True).call(input_local_dir)364all_filenames = self.filenames + self.symlink_files365all_filenames.sort()366result_list = []367for file_stat in file_stats:368result_list.append(getattr(file_stat, 'src'))369self.assertEqual(len(result_list), len(all_filenames))370# Just check to make sure the right local files are generated.371for i in range(len(result_list)):372filename = str(os.path.abspath(all_filenames[i]))373self.assertEqual(result_list[i], filename)374375376class TestListFilesLocally(unittest.TestCase):377maxDiff = None378379def setUp(self):380self.directory = str(tempfile.mkdtemp())381382def tearDown(self):383shutil.rmtree(self.directory)384385@mock.patch('os.listdir')386def test_error_raised_on_decoding_error(self, listdir_mock):387# On Python3, sys.getdefaultencoding388file_generator = FileGenerator(None, None, None)389# utf-8 encoding for U+2713.390listdir_mock.return_value = [b'\xe2\x9c\x93']391list(file_generator.list_files(self.directory, dir_op=True))392# Ensure the message was added to the result queue and is393# being skipped.394self.assertFalse(file_generator.result_queue.empty())395warning_message = file_generator.result_queue.get()396self.assertIn("warning: Skipping file ", warning_message.message)397self.assertIn("Please check your locale settings.",398warning_message.message)399400def test_list_files_is_in_sorted_order(self):401p = os.path.join402open(p(self.directory, 'test-123.txt'), 'w').close()403open(p(self.directory, 'test-321.txt'), 'w').close()404open(p(self.directory, 'test123.txt'), 'w').close()405open(p(self.directory, 'test321.txt'), 'w').close()406os.mkdir(p(self.directory, 'test'))407open(p(self.directory, 'test', 'foo.txt'), 'w').close()408409file_generator = FileGenerator(None, None, None)410values = list(el[0] for el in file_generator.list_files(411self.directory, dir_op=True))412ref_vals = list(sorted(values,413key=lambda items: items.replace(os.sep, '/')))414self.assertEqual(values, ref_vals)415416@mock.patch('awscli.customizations.s3.filegenerator.get_file_stat')417def test_list_files_with_invalid_timestamp(self, stat_mock):418stat_mock.return_value = 9, None419open(os.path.join(self.directory, 'test'), 'w').close()420file_generator = FileGenerator(None, None, None)421value = list(file_generator.list_files(self.directory, dir_op=True))[0]422self.assertIs(value[1]['LastModified'], EPOCH_TIME)423424def test_list_local_files_with_unicode_chars(self):425p = os.path.join426open(p(self.directory, u'a'), 'w').close()427open(p(self.directory, u'a\u0300'), 'w').close()428open(p(self.directory, u'a\u0300-1'), 'w').close()429open(p(self.directory, u'a\u03001'), 'w').close()430open(p(self.directory, u'z'), 'w').close()431open(p(self.directory, u'\u00e6'), 'w').close()432os.mkdir(p(self.directory, u'a\u0300a'))433open(p(self.directory, u'a\u0300a', u'a'), 'w').close()434open(p(self.directory, u'a\u0300a', u'z'), 'w').close()435open(p(self.directory, u'a\u0300a', u'\u00e6'), 'w').close()436437file_generator = FileGenerator(None, None, None)438values = list(el[0] for el in file_generator.list_files(439self.directory, dir_op=True))440expected_order = [os.path.join(self.directory, el) for el in [441u"a",442u"a\u0300",443u"a\u0300-1",444u"a\u03001",445u"a\u0300a%sa" % os.path.sep,446u"a\u0300a%sz" % os.path.sep,447u"a\u0300a%s\u00e6" % os.path.sep,448u"z",449u"\u00e6"450]]451self.assertEqual(values, expected_order)452453454class TestNormalizeSort(unittest.TestCase):455def test_normalize_sort(self):456names = ['xyz123456789',457'xyz1' + os.path.sep + 'test',458'xyz' + os.path.sep + 'test']459ref_names = [names[2], names[1], names[0]]460filegenerator = FileGenerator(None, None, None)461filegenerator.normalize_sort(names, os.path.sep, '/')462for i in range(len(ref_names)):463self.assertEqual(ref_names[i], names[i])464465def test_normalize_sort_backslash(self):466names = ['xyz123456789',467'xyz1\\test',468'xyz\\test']469ref_names = [names[2], names[1], names[0]]470filegenerator = FileGenerator(None, None, None)471filegenerator.normalize_sort(names, '\\', '/')472for i in range(len(ref_names)):473self.assertEqual(ref_names[i], names[i])474475476class S3FileGeneratorTest(BaseAWSCommandParamsTest):477def setUp(self):478super(S3FileGeneratorTest, self).setUp()479self.client = self.driver.session.create_client('s3')480self.bucket = 'foo'481self.file1 = self.bucket + '/' + 'text1.txt'482self.file2 = self.bucket + '/' + 'another_directory/text2.txt'483484def test_s3_file(self):485"""486Generate a single s3 file487Note: Size and last update are not tested because s3 generates them.488"""489input_s3_file = {'src': {'path': self.file1, 'type': 's3'},490'dest': {'path': 'text1.txt', 'type': 'local'},491'dir_op': False, 'use_src_name': False}492params = {'region': 'us-east-1'}493self.parsed_responses = [{"ETag": "abcd", "ContentLength": 100,494"LastModified": "2014-01-09T20:45:49.000Z"}]495self.patch_make_request()496497file_gen = FileGenerator(self.client, '')498files = file_gen.call(input_s3_file)499result_list = []500for filename in files:501result_list.append(filename)502file_stat = FileStat(src=self.file1, dest='text1.txt',503compare_key='text1.txt',504size=result_list[0].size,505last_update=result_list[0].last_update,506src_type='s3',507dest_type='local', operation_name='')508509ref_list = [file_stat]510self.assertEqual(len(result_list), len(ref_list))511for i in range(len(result_list)):512compare_files(self, result_list[i], ref_list[i])513514def test_s3_single_file_404(self):515"""516Test the error message for a 404 ClientError for a single file listing517"""518input_s3_file = {'src': {'path': self.file1, 'type': 's3'},519'dest': {'path': 'text1.txt', 'type': 'local'},520'dir_op': False, 'use_src_name': False}521params = {'region': 'us-east-1'}522self.client = mock.Mock()523self.client.head_object.side_effect = \524ClientError(525{'Error': {'Code': '404', 'Message': 'Not Found'}},526'HeadObject',527)528file_gen = FileGenerator(self.client, '')529files = file_gen.call(input_s3_file)530# The error should include 404 and should include the key name.531with self.assertRaisesRegex(ClientError, '404.*text1.txt'):532list(files)533534def test_s3_single_file_delete(self):535input_s3_file = {'src': {'path': self.file1, 'type': 's3'},536'dest': {'path': '', 'type': 'local'},537'dir_op': False, 'use_src_name': True}538self.client = mock.Mock()539file_gen = FileGenerator(self.client, 'delete')540result_list = list(file_gen.call(input_s3_file))541self.assertEqual(len(result_list), 1)542compare_files(543self,544result_list[0],545FileStat(src=self.file1, dest='text1.txt',546compare_key='text1.txt',547size=None, last_update=None,548src_type='s3', dest_type='local',549operation_name='delete')550)551self.client.head_object.assert_not_called()552553def test_s3_directory(self):554"""555Generates s3 files under a common prefix. Also it ensures that556zero size files are ignored.557Note: Size and last update are not tested because s3 generates them.558"""559input_s3_file = {'src': {'path': self.bucket + '/', 'type': 's3'},560'dest': {'path': '', 'type': 'local'},561'dir_op': True, 'use_src_name': True}562params = {'region': 'us-east-1'}563files = FileGenerator(self.client, '').call(input_s3_file)564565self.parsed_responses = [{566"CommonPrefixes": [], "Contents": [567{"Key": "another_directory/text2.txt", "Size": 100,568"LastModified": "2014-01-09T20:45:49.000Z"},569{"Key": "text1.txt", "Size": 10,570"LastModified": "2013-01-09T20:45:49.000Z"}]}]571self.patch_make_request()572result_list = []573for filename in files:574result_list.append(filename)575file_stat = FileStat(src=self.file2,576dest='another_directory' + os.sep +577'text2.txt',578compare_key='another_directory/text2.txt',579size=result_list[0].size,580last_update=result_list[0].last_update,581src_type='s3',582dest_type='local', operation_name='')583file_stat2 = FileStat(src=self.file1,584dest='text1.txt',585compare_key='text1.txt',586size=result_list[1].size,587last_update=result_list[1].last_update,588src_type='s3',589dest_type='local', operation_name='')590591ref_list = [file_stat, file_stat2]592self.assertEqual(len(result_list), len(ref_list))593for i in range(len(result_list)):594compare_files(self, result_list[i], ref_list[i])595596def test_s3_delete_directory(self):597"""598Generates s3 files under a common prefix. Also it ensures that599the directory itself is included because it is a delete command600Note: Size and last update are not tested because s3 generates them.601"""602input_s3_file = {'src': {'path': self.bucket + '/', 'type': 's3'},603'dest': {'path': '', 'type': 'local'},604'dir_op': True, 'use_src_name': True}605self.parsed_responses = [{606"CommonPrefixes": [], "Contents": [607{"Key": "another_directory/", "Size": 0,608"LastModified": "2012-01-09T20:45:49.000Z"},609{"Key": "another_directory/text2.txt", "Size": 100,610"LastModified": "2014-01-09T20:45:49.000Z"},611{"Key": "text1.txt", "Size": 10,612"LastModified": "2013-01-09T20:45:49.000Z"}]}]613self.patch_make_request()614files = FileGenerator(self.client, 'delete').call(input_s3_file)615result_list = []616for filename in files:617result_list.append(filename)618619file_stat1 = FileStat(src=self.bucket + '/another_directory/',620dest='another_directory' + os.sep,621compare_key='another_directory/',622size=result_list[0].size,623last_update=result_list[0].last_update,624src_type='s3',625dest_type='local', operation_name='delete')626file_stat2 = FileStat(src=self.file2,627dest='another_directory' + os.sep + 'text2.txt',628compare_key='another_directory/text2.txt',629size=result_list[1].size,630last_update=result_list[1].last_update,631src_type='s3',632dest_type='local', operation_name='delete')633file_stat3 = FileStat(src=self.file1,634dest='text1.txt',635compare_key='text1.txt',636size=result_list[2].size,637last_update=result_list[2].last_update,638src_type='s3',639dest_type='local', operation_name='delete')640641ref_list = [file_stat1, file_stat2, file_stat3]642self.assertEqual(len(result_list), len(ref_list))643for i in range(len(result_list)):644compare_files(self, result_list[i], ref_list[i])645646647if __name__ == "__main__":648unittest.main()649650651