archiver.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import os
  2. from io import StringIO
  3. import stat
  4. import subprocess
  5. import sys
  6. import shutil
  7. import tempfile
  8. import time
  9. import unittest
  10. from attic import xattr
  11. from attic.archiver import Archiver
  12. from attic.repository import Repository
  13. from attic.testsuite import AtticTestCase
  14. try:
  15. import llfuse
  16. has_llfuse = True
  17. except ImportError:
  18. has_llfuse = False
  19. src_dir = os.path.join(os.getcwd(), os.path.dirname(__file__), '..', '..')
  20. class changedir:
  21. def __init__(self, dir):
  22. self.dir = dir
  23. def __enter__(self):
  24. self.old = os.getcwd()
  25. os.chdir(self.dir)
  26. def __exit__(self, *args, **kw):
  27. os.chdir(self.old)
  28. class ArchiverTestCase(AtticTestCase):
  29. prefix = ''
  30. def setUp(self):
  31. self.archiver = Archiver()
  32. self.tmpdir = tempfile.mkdtemp()
  33. self.repository_path = os.path.join(self.tmpdir, 'repository')
  34. self.repository_location = self.prefix + self.repository_path
  35. self.input_path = os.path.join(self.tmpdir, 'input')
  36. self.output_path = os.path.join(self.tmpdir, 'output')
  37. self.keys_path = os.path.join(self.tmpdir, 'keys')
  38. self.cache_path = os.path.join(self.tmpdir, 'cache')
  39. os.environ['ATTIC_KEYS_DIR'] = self.keys_path
  40. os.environ['ATTIC_CACHE_DIR'] = self.cache_path
  41. os.mkdir(self.input_path)
  42. os.mkdir(self.output_path)
  43. os.mkdir(self.keys_path)
  44. os.mkdir(self.cache_path)
  45. self._old_wd = os.getcwd()
  46. os.chdir(self.tmpdir)
  47. def tearDown(self):
  48. shutil.rmtree(self.tmpdir)
  49. os.chdir(self._old_wd)
  50. def attic(self, *args, **kw):
  51. exit_code = kw.get('exit_code', 0)
  52. fork = kw.get('fork', False)
  53. if fork:
  54. try:
  55. output = subprocess.check_output((sys.executable, '-m', 'attic.archiver') + args)
  56. ret = 0
  57. except subprocess.CalledProcessError as e:
  58. output = e.output
  59. ret = e.returncode
  60. output = os.fsdecode(output)
  61. if ret != exit_code:
  62. print(output)
  63. self.assert_equal(exit_code, ret)
  64. return output
  65. args = list(args)
  66. try:
  67. stdout, stderr = sys.stdout, sys.stderr
  68. output = StringIO()
  69. sys.stdout = sys.stderr = output
  70. ret = self.archiver.run(args)
  71. sys.stdout, sys.stderr = stdout, stderr
  72. if ret != exit_code:
  73. print(output.getvalue())
  74. self.assert_equal(exit_code, ret)
  75. return output.getvalue()
  76. finally:
  77. sys.stdout, sys.stderr = stdout, stderr
  78. def create_src_archive(self, name):
  79. self.attic('init', self.repository_location)
  80. self.attic('create', self.repository_location + '::' + name, src_dir)
  81. def create_regual_file(self, name, size=0):
  82. filename = os.path.join(self.input_path, name)
  83. if not os.path.exists(os.path.dirname(filename)):
  84. os.makedirs(os.path.dirname(filename))
  85. with open(filename, 'wb') as fd:
  86. fd.write(b'X' * size)
  87. def create_test_files(self):
  88. """Create a minimal test case including all supported file types
  89. """
  90. # File
  91. self.create_regual_file('file1', size=1024 * 80)
  92. # Directory
  93. self.create_regual_file('dir2/file2', size=1024 * 80)
  94. # File owner
  95. os.chown('input/file1', 100, 200)
  96. # File mode
  97. os.chmod('input/file1', 0o7755)
  98. os.chmod('input/dir2', 0o555)
  99. # Block device
  100. os.mknod('input/bdev', 0o600 | stat.S_IFBLK, os.makedev(10, 20))
  101. # Char device
  102. os.mknod('input/cdev', 0o600 | stat.S_IFCHR, os.makedev(30, 40))
  103. if xattr.is_enabled():
  104. xattr.setxattr(os.path.join(self.input_path, 'file1'), 'user.foo', b'bar')
  105. # Hard link
  106. os.link(os.path.join(self.input_path, 'file1'),
  107. os.path.join(self.input_path, 'hardlink'))
  108. # Symlink
  109. os.symlink('somewhere', os.path.join(self.input_path, 'link1'))
  110. # FIFO node
  111. os.mkfifo(os.path.join(self.input_path, 'fifo1'))
  112. def test_basic_functionality(self):
  113. self.create_test_files()
  114. self.attic('init', self.repository_location)
  115. self.attic('create', self.repository_location + '::test', 'input')
  116. self.attic('create', self.repository_location + '::test.2', 'input')
  117. with changedir('output'):
  118. self.attic('extract', self.repository_location + '::test')
  119. self.assert_equal(len(self.attic('list', self.repository_location).splitlines()), 2)
  120. self.assert_equal(len(self.attic('list', self.repository_location + '::test').splitlines()), 9)
  121. self.assert_dirs_equal('input', 'output/input')
  122. info_output = self.attic('info', self.repository_location + '::test')
  123. shutil.rmtree(self.cache_path)
  124. info_output2 = self.attic('info', self.repository_location + '::test')
  125. # info_output2 starts with some "initializing cache" text but should
  126. # end the same way as info_output
  127. assert info_output2.endswith(info_output)
  128. def test_extract_include_exclude(self):
  129. self.attic('init', self.repository_location)
  130. self.create_regual_file('file1', size=1024 * 80)
  131. self.create_regual_file('file2', size=1024 * 80)
  132. self.create_regual_file('file3', size=1024 * 80)
  133. self.create_regual_file('file4', size=1024 * 80)
  134. self.attic('create', '--exclude=input/file4', self.repository_location + '::test', 'input')
  135. with changedir('output'):
  136. self.attic('extract', self.repository_location + '::test', 'input/file1', )
  137. self.assert_equal(sorted(os.listdir('output/input')), ['file1'])
  138. with changedir('output'):
  139. self.attic('extract', '--exclude=input/file2', self.repository_location + '::test')
  140. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  141. def test_path_normalization(self):
  142. self.attic('init', self.repository_location)
  143. self.create_regual_file('dir1/dir2/file', size=1024 * 80)
  144. with changedir('input/dir1/dir2'):
  145. self.attic('create', self.repository_location + '::test', '../../../input/dir1/../dir1/dir2/..')
  146. output = self.attic('list', self.repository_location + '::test')
  147. self.assert_not_in('..', output)
  148. self.assert_in(' input/dir1/dir2/file', output)
  149. def test_overwrite(self):
  150. self.create_regual_file('file1', size=1024 * 80)
  151. self.create_regual_file('dir2/file2', size=1024 * 80)
  152. self.attic('init', self.repository_location)
  153. self.attic('create', self.repository_location + '::test', 'input')
  154. # Overwriting regular files and directories should be supported
  155. os.mkdir('output/input')
  156. os.mkdir('output/input/file1')
  157. os.mkdir('output/input/dir2')
  158. with changedir('output'):
  159. self.attic('extract', self.repository_location + '::test')
  160. self.assert_dirs_equal('input', 'output/input')
  161. # But non-empty dirs should fail
  162. os.unlink('output/input/file1')
  163. os.mkdir('output/input/file1')
  164. os.mkdir('output/input/file1/dir')
  165. with changedir('output'):
  166. self.attic('extract', self.repository_location + '::test', exit_code=1)
  167. def test_delete(self):
  168. self.create_regual_file('file1', size=1024 * 80)
  169. self.create_regual_file('dir2/file2', size=1024 * 80)
  170. self.attic('init', self.repository_location)
  171. self.attic('create', self.repository_location + '::test', 'input')
  172. self.attic('create', self.repository_location + '::test.2', 'input')
  173. self.attic('verify', self.repository_location + '::test')
  174. self.attic('verify', self.repository_location + '::test.2')
  175. self.attic('delete', self.repository_location + '::test')
  176. self.attic('verify', self.repository_location + '::test.2')
  177. self.attic('delete', self.repository_location + '::test.2')
  178. # Make sure all data except the manifest has been deleted
  179. repository = Repository(self.repository_path)
  180. self.assert_equal(repository._len(), 1)
  181. def test_corrupted_repository(self):
  182. self.create_src_archive('test')
  183. self.attic('verify', self.repository_location + '::test')
  184. name = sorted(os.listdir(os.path.join(self.tmpdir, 'repository', 'data', '0')), reverse=True)[0]
  185. fd = open(os.path.join(self.tmpdir, 'repository', 'data', '0', name), 'r+')
  186. fd.seek(100)
  187. fd.write('X')
  188. fd.close()
  189. self.attic('verify', self.repository_location + '::test', exit_code=1)
  190. def test_prune_repository(self):
  191. self.attic('init', self.repository_location)
  192. self.attic('create', self.repository_location + '::test1', src_dir)
  193. self.attic('create', self.repository_location + '::test2', src_dir)
  194. self.attic('prune', self.repository_location, '--daily=2')
  195. output = self.attic('list', self.repository_location)
  196. assert 'test1' not in output
  197. assert 'test2' in output
  198. def test_usage(self):
  199. self.assert_raises(SystemExit, lambda: self.attic())
  200. self.assert_raises(SystemExit, lambda: self.attic('-h'))
  201. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  202. def test_mount(self):
  203. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  204. os.mkdir(mountpoint)
  205. self.attic('init', self.repository_location)
  206. self.create_test_files()
  207. self.attic('create', self.repository_location + '::archive', 'input')
  208. try:
  209. self.attic('mount', self.repository_location + '::archive', mountpoint, fork=True)
  210. self.wait_for_mount(mountpoint)
  211. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'input'))
  212. finally:
  213. if sys.platform.startswith('linux'):
  214. os.system('fusermount -u ' + mountpoint)
  215. else:
  216. os.system('umount ' + mountpoint)
  217. os.rmdir(mountpoint)
  218. # Give the daemon some time to exit
  219. time.sleep(.2)
  220. class RemoteArchiverTestCase(ArchiverTestCase):
  221. prefix = '__testsuite__:'