2
0

archiver.py 11 KB

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