archiver.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import filecmp
  2. import os
  3. from io import StringIO
  4. import stat
  5. import sys
  6. import shutil
  7. import tempfile
  8. from attic import xattr
  9. from attic.archiver import Archiver
  10. from attic.repository import Repository
  11. from attic.testsuite import AtticTestCase
  12. has_mtime_ns = sys.version >= '3.3'
  13. utime_supports_fd = os.utime in getattr(os, 'supports_fd', {})
  14. src_dir = os.path.join(os.getcwd(), os.path.dirname(__file__), '..', '..')
  15. class changedir:
  16. def __init__(self, dir):
  17. self.dir = dir
  18. def __enter__(self):
  19. self.old = os.getcwd()
  20. os.chdir(self.dir)
  21. def __exit__(self, *args, **kw):
  22. os.chdir(self.old)
  23. class ArchiverTestCase(AtticTestCase):
  24. prefix = ''
  25. def setUp(self):
  26. self.archiver = Archiver()
  27. self.tmpdir = tempfile.mkdtemp()
  28. self.repository_path = os.path.join(self.tmpdir, 'repository')
  29. self.repository_location = self.prefix + self.repository_path
  30. self.input_path = os.path.join(self.tmpdir, 'input')
  31. self.output_path = os.path.join(self.tmpdir, 'output')
  32. self.keys_path = os.path.join(self.tmpdir, 'keys')
  33. self.cache_path = os.path.join(self.tmpdir, 'cache')
  34. os.environ['ATTIC_KEYS_DIR'] = self.keys_path
  35. os.environ['ATTIC_CACHE_DIR'] = self.cache_path
  36. os.mkdir(self.input_path)
  37. os.mkdir(self.output_path)
  38. os.mkdir(self.keys_path)
  39. os.mkdir(self.cache_path)
  40. self._old_wd = os.getcwd()
  41. os.chdir(self.tmpdir)
  42. def tearDown(self):
  43. shutil.rmtree(self.tmpdir)
  44. os.chdir(self._old_wd)
  45. def attic(self, *args, **kwargs):
  46. exit_code = kwargs.get('exit_code', 0)
  47. args = list(args)
  48. try:
  49. stdout, stderr = sys.stdout, sys.stderr
  50. output = StringIO()
  51. sys.stdout = sys.stderr = output
  52. ret = self.archiver.run(args)
  53. sys.stdout, sys.stderr = stdout, stderr
  54. if ret != exit_code:
  55. print(output.getvalue())
  56. self.assert_equal(exit_code, ret)
  57. return output.getvalue()
  58. finally:
  59. sys.stdout, sys.stderr = stdout, stderr
  60. def create_src_archive(self, name):
  61. self.attic('init', self.repository_location)
  62. self.attic('create', self.repository_location + '::' + name, src_dir)
  63. def create_regual_file(self, name, size=0):
  64. filename = os.path.join(self.input_path, name)
  65. if not os.path.exists(os.path.dirname(filename)):
  66. os.makedirs(os.path.dirname(filename))
  67. with open(filename, 'wb') as fd:
  68. fd.write(b'X' * size)
  69. def get_xattrs(self, path):
  70. try:
  71. return xattr.get_all(path)
  72. except EnvironmentError:
  73. return {}
  74. def diff_dirs(self, dir1, dir2):
  75. diff = filecmp.dircmp(dir1, dir2)
  76. self.assert_equal(diff.left_only, [])
  77. self.assert_equal(diff.right_only, [])
  78. self.assert_equal(diff.diff_files, [])
  79. for filename in diff.common:
  80. path1 = os.path.join(dir1, filename)
  81. path2 = os.path.join(dir2, filename)
  82. s1 = os.lstat(path1)
  83. s2 = os.lstat(path2)
  84. attrs = ['st_mode', 'st_uid', 'st_gid', 'st_rdev']
  85. if not os.path.islink(path1) or utime_supports_fd:
  86. attrs.append('st_mtime_ns' if has_mtime_ns else 'st_mtime')
  87. d1 = [filename] + [getattr(s1, a) for a in attrs]
  88. d2 = [filename] + [getattr(s2, a) for a in attrs]
  89. # 'st_mtime precision is limited'
  90. if attrs[-1] == 'st_mtime':
  91. d1[-1] = round(d1[-1], 2)
  92. d2[-1] = round(d2[-1], 2)
  93. d1.append(self.get_xattrs(path1))
  94. d2.append(self.get_xattrs(path2))
  95. self.assert_equal(d1, d2)
  96. def test_basic_functionality(self):
  97. # File
  98. self.create_regual_file('file1', size=1024 * 80)
  99. # Directory
  100. self.create_regual_file('dir2/file2', size=1024 * 80)
  101. # File owner
  102. os.chown('input/file1', 100, 200)
  103. # File mode
  104. os.chmod('input/file1', 0o7755)
  105. os.chmod('input/dir2', 0o700)
  106. # Block device
  107. os.mknod('input/bdev', 0o600 | stat.S_IFBLK, os.makedev(10, 20))
  108. # Char device
  109. os.mknod('input/cdev', 0o600 | stat.S_IFCHR, os.makedev(30, 40))
  110. if xattr.is_enabled():
  111. xattr.set(os.path.join(self.input_path, 'file1'), b'foo', b'bar')
  112. # Hard link
  113. os.link(os.path.join(self.input_path, 'file1'),
  114. os.path.join(self.input_path, 'hardlink'))
  115. # Symlink
  116. os.symlink('somewhere', os.path.join(self.input_path, 'link1'))
  117. # FIFO node
  118. os.mkfifo(os.path.join(self.input_path, 'fifo1'))
  119. self.attic('init', self.repository_location)
  120. self.attic('create', self.repository_location + '::test', 'input')
  121. self.attic('create', self.repository_location + '::test.2', 'input')
  122. with changedir('output'):
  123. self.attic('extract', self.repository_location + '::test')
  124. self.assert_equal(len(self.attic('list', self.repository_location).splitlines()), 2)
  125. self.assert_equal(len(self.attic('list', self.repository_location + '::test').splitlines()), 9)
  126. self.diff_dirs('input', 'output/input')
  127. info_output = self.attic('info', self.repository_location + '::test')
  128. shutil.rmtree(self.cache_path)
  129. info_output2 = self.attic('info', self.repository_location + '::test')
  130. # info_output2 starts with some "initializing cache" text but should
  131. # end the same way as info_output
  132. assert info_output2.endswith(info_output)
  133. def test_extract_include_exclude(self):
  134. self.attic('init', self.repository_location)
  135. self.create_regual_file('file1', size=1024 * 80)
  136. self.create_regual_file('file2', size=1024 * 80)
  137. self.create_regual_file('file3', size=1024 * 80)
  138. self.create_regual_file('file4', size=1024 * 80)
  139. self.attic('create', '--exclude=input/file4', self.repository_location + '::test', 'input')
  140. with changedir('output'):
  141. self.attic('extract', self.repository_location + '::test', 'input/file1', )
  142. self.assert_equal(sorted(os.listdir('output/input')), ['file1'])
  143. with changedir('output'):
  144. self.attic('extract', '--exclude=input/file2', self.repository_location + '::test')
  145. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  146. def test_overwrite(self):
  147. self.create_regual_file('file1', size=1024 * 80)
  148. self.create_regual_file('dir2/file2', size=1024 * 80)
  149. self.attic('init', self.repository_location)
  150. self.attic('create', self.repository_location + '::test', 'input')
  151. # Overwriting regular files and directories should be supported
  152. os.mkdir('output/input')
  153. os.mkdir('output/input/file1')
  154. os.mkdir('output/input/dir2')
  155. with changedir('output'):
  156. self.attic('extract', self.repository_location + '::test')
  157. self.diff_dirs('input', 'output/input')
  158. # But non-empty dirs should fail
  159. os.unlink('output/input/file1')
  160. os.mkdir('output/input/file1')
  161. os.mkdir('output/input/file1/dir')
  162. with changedir('output'):
  163. self.attic('extract', self.repository_location + '::test', exit_code=1)
  164. def test_delete(self):
  165. self.create_regual_file('file1', size=1024 * 80)
  166. self.create_regual_file('dir2/file2', size=1024 * 80)
  167. self.attic('init', self.repository_location)
  168. self.attic('create', self.repository_location + '::test', 'input')
  169. self.attic('create', self.repository_location + '::test.2', 'input')
  170. self.attic('verify', self.repository_location + '::test')
  171. self.attic('verify', self.repository_location + '::test.2')
  172. self.attic('delete', self.repository_location + '::test')
  173. self.attic('verify', self.repository_location + '::test.2')
  174. self.attic('delete', self.repository_location + '::test.2')
  175. # Make sure all data except the manifest has been deleted
  176. repository = Repository(self.repository_path)
  177. self.assert_equal(repository._len(), 1)
  178. def test_corrupted_repository(self):
  179. self.create_src_archive('test')
  180. self.attic('verify', self.repository_location + '::test')
  181. name = sorted(os.listdir(os.path.join(self.tmpdir, 'repository', 'data', '0')), reverse=True)[0]
  182. fd = open(os.path.join(self.tmpdir, 'repository', 'data', '0', name), 'r+')
  183. fd.seek(100)
  184. fd.write('X')
  185. fd.close()
  186. self.attic('verify', self.repository_location + '::test', exit_code=1)
  187. def test_prune_repository(self):
  188. self.attic('init', self.repository_location)
  189. self.attic('create', self.repository_location + '::test1', src_dir)
  190. self.attic('create', self.repository_location + '::test2', src_dir)
  191. self.attic('prune', self.repository_location, '--daily=2')
  192. output = self.attic('list', self.repository_location)
  193. assert 'test1' not in output
  194. assert 'test2' in output
  195. def test_usage(self):
  196. self.assert_raises(SystemExit, lambda: self.attic())
  197. self.assert_raises(SystemExit, lambda: self.attic('-h'))
  198. class RemoteArchiverTestCase(ArchiverTestCase):
  199. prefix = '__testsuite__:'