archiver.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. from binascii import hexlify
  2. from configparser import RawConfigParser
  3. import os
  4. from io import StringIO
  5. import stat
  6. import subprocess
  7. import sys
  8. import shutil
  9. import tempfile
  10. import time
  11. import unittest
  12. from hashlib import sha256
  13. from attic import xattr
  14. from attic.archive import Archive, ChunkBuffer
  15. from attic.archiver import Archiver
  16. from attic.cache import Cache
  17. from attic.crypto import bytes16_to_int, num_aes_blocks
  18. from attic.helpers import Manifest
  19. from attic.key import parser
  20. from attic.remote import RemoteRepository, PathNotAllowed
  21. from attic.repository import Repository
  22. from attic.testsuite import AtticTestCase
  23. from attic.testsuite.mock import patch
  24. try:
  25. import llfuse
  26. has_llfuse = True
  27. except ImportError:
  28. has_llfuse = False
  29. has_lchflags = hasattr(os, 'lchflags')
  30. src_dir = os.path.join(os.getcwd(), os.path.dirname(__file__), '..')
  31. class changedir:
  32. def __init__(self, dir):
  33. self.dir = dir
  34. def __enter__(self):
  35. self.old = os.getcwd()
  36. os.chdir(self.dir)
  37. def __exit__(self, *args, **kw):
  38. os.chdir(self.old)
  39. class environment_variable:
  40. def __init__(self, **values):
  41. self.values = values
  42. self.old_values = {}
  43. def __enter__(self):
  44. for k, v in self.values.items():
  45. self.old_values[k] = os.environ.get(k)
  46. os.environ[k] = v
  47. def __exit__(self, *args, **kw):
  48. for k, v in self.old_values.items():
  49. if v is not None:
  50. os.environ[k] = v
  51. class ArchiverTestCaseBase(AtticTestCase):
  52. prefix = ''
  53. def setUp(self):
  54. os.environ['ATTIC_CHECK_I_KNOW_WHAT_I_AM_DOING'] = '1'
  55. self.archiver = Archiver()
  56. self.tmpdir = tempfile.mkdtemp()
  57. self.repository_path = os.path.join(self.tmpdir, 'repository')
  58. self.repository_location = self.prefix + self.repository_path
  59. self.input_path = os.path.join(self.tmpdir, 'input')
  60. self.output_path = os.path.join(self.tmpdir, 'output')
  61. self.keys_path = os.path.join(self.tmpdir, 'keys')
  62. self.cache_path = os.path.join(self.tmpdir, 'cache')
  63. self.exclude_file_path = os.path.join(self.tmpdir, 'excludes')
  64. os.environ['ATTIC_KEYS_DIR'] = self.keys_path
  65. os.environ['ATTIC_CACHE_DIR'] = self.cache_path
  66. os.mkdir(self.input_path)
  67. os.mkdir(self.output_path)
  68. os.mkdir(self.keys_path)
  69. os.mkdir(self.cache_path)
  70. with open(self.exclude_file_path, 'wb') as fd:
  71. fd.write(b'input/file2\n# A commment line, then a blank line\n\n')
  72. self._old_wd = os.getcwd()
  73. os.chdir(self.tmpdir)
  74. def tearDown(self):
  75. shutil.rmtree(self.tmpdir)
  76. os.chdir(self._old_wd)
  77. def attic(self, *args, **kw):
  78. exit_code = kw.get('exit_code', 0)
  79. fork = kw.get('fork', False)
  80. if fork:
  81. try:
  82. output = subprocess.check_output((sys.executable, '-m', 'attic.archiver') + args)
  83. ret = 0
  84. except subprocess.CalledProcessError as e:
  85. output = e.output
  86. ret = e.returncode
  87. output = os.fsdecode(output)
  88. if ret != exit_code:
  89. print(output)
  90. self.assert_equal(exit_code, ret)
  91. return output
  92. args = list(args)
  93. stdout, stderr = sys.stdout, sys.stderr
  94. try:
  95. output = StringIO()
  96. sys.stdout = sys.stderr = output
  97. ret = self.archiver.run(args)
  98. sys.stdout, sys.stderr = stdout, stderr
  99. if ret != exit_code:
  100. print(output.getvalue())
  101. self.assert_equal(exit_code, ret)
  102. return output.getvalue()
  103. finally:
  104. sys.stdout, sys.stderr = stdout, stderr
  105. def create_src_archive(self, name):
  106. self.attic('create', self.repository_location + '::' + name, src_dir)
  107. class ArchiverTestCase(ArchiverTestCaseBase):
  108. def create_regular_file(self, name, size=0, contents=None):
  109. filename = os.path.join(self.input_path, name)
  110. if not os.path.exists(os.path.dirname(filename)):
  111. os.makedirs(os.path.dirname(filename))
  112. with open(filename, 'wb') as fd:
  113. if contents is None:
  114. contents = b'X' * size
  115. fd.write(contents)
  116. def create_test_files(self):
  117. """Create a minimal test case including all supported file types
  118. """
  119. # File
  120. self.create_regular_file('empty', size=0)
  121. # next code line raises OverflowError on 32bit cpu (raspberry pi 2):
  122. # 2600-01-01 > 2**64 ns
  123. #os.utime('input/empty', (19880895600, 19880895600))
  124. # thus, we better test with something not that far in future:
  125. # 2038-01-19 (1970 + 2^31 - 1 seconds) is the 32bit "deadline":
  126. os.utime('input/empty', (2**31 - 1, 2**31 - 1))
  127. self.create_regular_file('file1', size=1024 * 80)
  128. self.create_regular_file('flagfile', size=1024)
  129. # Directory
  130. self.create_regular_file('dir2/file2', size=1024 * 80)
  131. # File owner
  132. os.chown('input/file1', 100, 200)
  133. # File mode
  134. os.chmod('input/file1', 0o7755)
  135. os.chmod('input/dir2', 0o555)
  136. # Block device
  137. os.mknod('input/bdev', 0o600 | stat.S_IFBLK, os.makedev(10, 20))
  138. # Char device
  139. os.mknod('input/cdev', 0o600 | stat.S_IFCHR, os.makedev(30, 40))
  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. if xattr.is_enabled(self.input_path):
  146. xattr.setxattr(os.path.join(self.input_path, 'file1'), 'user.foo', b'bar')
  147. # XXX this always fails for me
  148. # ubuntu 14.04, on a TMP dir filesystem with user_xattr, using fakeroot
  149. # same for newer ubuntu and centos.
  150. # if this is supported just on specific platform, platform should be checked first,
  151. # so that the test setup for all tests using it does not fail here always for others.
  152. #xattr.setxattr(os.path.join(self.input_path, 'link1'), 'user.foo_symlink', b'bar_symlink', follow_symlinks=False)
  153. # FIFO node
  154. os.mkfifo(os.path.join(self.input_path, 'fifo1'))
  155. if has_lchflags:
  156. os.lchflags(os.path.join(self.input_path, 'flagfile'), stat.UF_NODUMP)
  157. def test_basic_functionality(self):
  158. self.create_test_files()
  159. self.attic('init', self.repository_location)
  160. self.attic('create', self.repository_location + '::test', 'input')
  161. self.attic('create', self.repository_location + '::test.2', 'input')
  162. with changedir('output'):
  163. self.attic('extract', self.repository_location + '::test')
  164. self.assert_equal(len(self.attic('list', self.repository_location).splitlines()), 2)
  165. self.assert_equal(len(self.attic('list', self.repository_location + '::test').splitlines()), 11)
  166. self.assert_dirs_equal('input', 'output/input')
  167. info_output = self.attic('info', self.repository_location + '::test')
  168. self.assert_in('Number of files: 4', info_output)
  169. shutil.rmtree(self.cache_path)
  170. with environment_variable(ATTIC_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='1'):
  171. info_output2 = self.attic('info', self.repository_location + '::test')
  172. # info_output2 starts with some "initializing cache" text but should
  173. # end the same way as info_output
  174. assert info_output2.endswith(info_output)
  175. def _extract_repository_id(self, path):
  176. return Repository(self.repository_path).id
  177. def _set_repository_id(self, path, id):
  178. config = RawConfigParser()
  179. config.read(os.path.join(path, 'config'))
  180. config.set('repository', 'id', hexlify(id).decode('ascii'))
  181. with open(os.path.join(path, 'config'), 'w') as fd:
  182. config.write(fd)
  183. return Repository(self.repository_path).id
  184. def test_repository_swap_detection(self):
  185. self.create_test_files()
  186. os.environ['ATTIC_PASSPHRASE'] = 'passphrase'
  187. self.attic('init', '--encryption=passphrase', self.repository_location)
  188. repository_id = self._extract_repository_id(self.repository_path)
  189. self.attic('create', self.repository_location + '::test', 'input')
  190. shutil.rmtree(self.repository_path)
  191. self.attic('init', '--encryption=none', self.repository_location)
  192. self._set_repository_id(self.repository_path, repository_id)
  193. self.assert_equal(repository_id, self._extract_repository_id(self.repository_path))
  194. self.assert_raises(Cache.EncryptionMethodMismatch, lambda :self.attic('create', self.repository_location + '::test.2', 'input'))
  195. def test_strip_components(self):
  196. self.attic('init', self.repository_location)
  197. self.create_regular_file('dir/file')
  198. self.attic('create', self.repository_location + '::test', 'input')
  199. with changedir('output'):
  200. self.attic('extract', self.repository_location + '::test', '--strip-components', '3')
  201. self.assert_true(not os.path.exists('file'))
  202. with self.assert_creates_file('file'):
  203. self.attic('extract', self.repository_location + '::test', '--strip-components', '2')
  204. with self.assert_creates_file('dir/file'):
  205. self.attic('extract', self.repository_location + '::test', '--strip-components', '1')
  206. with self.assert_creates_file('input/dir/file'):
  207. self.attic('extract', self.repository_location + '::test', '--strip-components', '0')
  208. def test_extract_include_exclude(self):
  209. self.attic('init', self.repository_location)
  210. self.create_regular_file('file1', size=1024 * 80)
  211. self.create_regular_file('file2', size=1024 * 80)
  212. self.create_regular_file('file3', size=1024 * 80)
  213. self.create_regular_file('file4', size=1024 * 80)
  214. self.attic('create', '--exclude=input/file4', self.repository_location + '::test', 'input')
  215. with changedir('output'):
  216. self.attic('extract', self.repository_location + '::test', 'input/file1', )
  217. self.assert_equal(sorted(os.listdir('output/input')), ['file1'])
  218. with changedir('output'):
  219. self.attic('extract', '--exclude=input/file2', self.repository_location + '::test')
  220. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  221. with changedir('output'):
  222. self.attic('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  223. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  224. def test_exclude_caches(self):
  225. self.attic('init', self.repository_location)
  226. self.create_regular_file('file1', size=1024 * 80)
  227. self.create_regular_file('cache1/CACHEDIR.TAG', contents=b'Signature: 8a477f597d28d172789f06886806bc55 extra stuff')
  228. self.create_regular_file('cache2/CACHEDIR.TAG', contents=b'invalid signature')
  229. self.attic('create', '--exclude-caches', self.repository_location + '::test', 'input')
  230. with changedir('output'):
  231. self.attic('extract', self.repository_location + '::test')
  232. self.assert_equal(sorted(os.listdir('output/input')), ['cache2', 'file1'])
  233. self.assert_equal(sorted(os.listdir('output/input/cache2')), ['CACHEDIR.TAG'])
  234. def test_path_normalization(self):
  235. self.attic('init', self.repository_location)
  236. self.create_regular_file('dir1/dir2/file', size=1024 * 80)
  237. with changedir('input/dir1/dir2'):
  238. self.attic('create', self.repository_location + '::test', '../../../input/dir1/../dir1/dir2/..')
  239. output = self.attic('list', self.repository_location + '::test')
  240. self.assert_not_in('..', output)
  241. self.assert_in(' input/dir1/dir2/file', output)
  242. def test_exclude_normalization(self):
  243. self.attic('init', self.repository_location)
  244. self.create_regular_file('file1', size=1024 * 80)
  245. self.create_regular_file('file2', size=1024 * 80)
  246. with changedir('input'):
  247. self.attic('create', '--exclude=file1', self.repository_location + '::test1', '.')
  248. with changedir('output'):
  249. self.attic('extract', self.repository_location + '::test1')
  250. self.assert_equal(sorted(os.listdir('output')), ['file2'])
  251. with changedir('input'):
  252. self.attic('create', '--exclude=./file1', self.repository_location + '::test2', '.')
  253. with changedir('output'):
  254. self.attic('extract', self.repository_location + '::test2')
  255. self.assert_equal(sorted(os.listdir('output')), ['file2'])
  256. self.attic('create', '--exclude=input/./file1', self.repository_location + '::test3', 'input')
  257. with changedir('output'):
  258. self.attic('extract', self.repository_location + '::test3')
  259. self.assert_equal(sorted(os.listdir('output/input')), ['file2'])
  260. def test_repeated_files(self):
  261. self.create_regular_file('file1', size=1024 * 80)
  262. self.attic('init', self.repository_location)
  263. self.attic('create', self.repository_location + '::test', 'input', 'input')
  264. def test_overwrite(self):
  265. self.create_regular_file('file1', size=1024 * 80)
  266. self.create_regular_file('dir2/file2', size=1024 * 80)
  267. self.attic('init', self.repository_location)
  268. self.attic('create', self.repository_location + '::test', 'input')
  269. # Overwriting regular files and directories should be supported
  270. os.mkdir('output/input')
  271. os.mkdir('output/input/file1')
  272. os.mkdir('output/input/dir2')
  273. with changedir('output'):
  274. self.attic('extract', self.repository_location + '::test')
  275. self.assert_dirs_equal('input', 'output/input')
  276. # But non-empty dirs should fail
  277. os.unlink('output/input/file1')
  278. os.mkdir('output/input/file1')
  279. os.mkdir('output/input/file1/dir')
  280. with changedir('output'):
  281. self.attic('extract', self.repository_location + '::test', exit_code=1)
  282. def test_rename(self):
  283. self.create_regular_file('file1', size=1024 * 80)
  284. self.create_regular_file('dir2/file2', size=1024 * 80)
  285. self.attic('init', self.repository_location)
  286. self.attic('create', self.repository_location + '::test', 'input')
  287. self.attic('create', self.repository_location + '::test.2', 'input')
  288. self.attic('extract', '--dry-run', self.repository_location + '::test')
  289. self.attic('extract', '--dry-run', self.repository_location + '::test.2')
  290. self.attic('rename', self.repository_location + '::test', 'test.3')
  291. self.attic('extract', '--dry-run', self.repository_location + '::test.2')
  292. self.attic('rename', self.repository_location + '::test.2', 'test.4')
  293. self.attic('extract', '--dry-run', self.repository_location + '::test.3')
  294. self.attic('extract', '--dry-run', self.repository_location + '::test.4')
  295. # Make sure both archives have been renamed
  296. repository = Repository(self.repository_path)
  297. manifest, key = Manifest.load(repository)
  298. self.assert_equal(len(manifest.archives), 2)
  299. self.assert_in('test.3', manifest.archives)
  300. self.assert_in('test.4', manifest.archives)
  301. def test_delete(self):
  302. self.create_regular_file('file1', size=1024 * 80)
  303. self.create_regular_file('dir2/file2', size=1024 * 80)
  304. self.attic('init', self.repository_location)
  305. self.attic('create', self.repository_location + '::test', 'input')
  306. self.attic('create', self.repository_location + '::test.2', 'input')
  307. self.attic('extract', '--dry-run', self.repository_location + '::test')
  308. self.attic('extract', '--dry-run', self.repository_location + '::test.2')
  309. self.attic('delete', self.repository_location + '::test')
  310. self.attic('extract', '--dry-run', self.repository_location + '::test.2')
  311. self.attic('delete', self.repository_location + '::test.2')
  312. # Make sure all data except the manifest has been deleted
  313. repository = Repository(self.repository_path)
  314. self.assert_equal(len(repository), 1)
  315. def test_corrupted_repository(self):
  316. self.attic('init', self.repository_location)
  317. self.create_src_archive('test')
  318. self.attic('extract', '--dry-run', self.repository_location + '::test')
  319. self.attic('check', self.repository_location)
  320. name = sorted(os.listdir(os.path.join(self.tmpdir, 'repository', 'data', '0')), reverse=True)[0]
  321. with open(os.path.join(self.tmpdir, 'repository', 'data', '0', name), 'r+') as fd:
  322. fd.seek(100)
  323. fd.write('XXXX')
  324. self.attic('check', self.repository_location, exit_code=1)
  325. def test_readonly_repository(self):
  326. self.attic('init', self.repository_location)
  327. self.create_src_archive('test')
  328. os.system('chmod -R ugo-w ' + self.repository_path)
  329. try:
  330. self.attic('extract', '--dry-run', self.repository_location + '::test')
  331. finally:
  332. # Restore permissions so shutil.rmtree is able to delete it
  333. os.system('chmod -R u+w ' + self.repository_path)
  334. def test_cmdline_compatibility(self):
  335. self.create_regular_file('file1', size=1024 * 80)
  336. self.attic('init', self.repository_location)
  337. self.attic('create', self.repository_location + '::test', 'input')
  338. output = self.attic('verify', '-v', self.repository_location + '::test')
  339. self.assert_in('"attic verify" has been deprecated', output)
  340. output = self.attic('prune', self.repository_location, '--hourly=1')
  341. self.assert_in('"--hourly" has been deprecated. Use "--keep-hourly" instead', output)
  342. def test_prune_repository(self):
  343. self.attic('init', self.repository_location)
  344. self.attic('create', self.repository_location + '::test1', src_dir)
  345. self.attic('create', self.repository_location + '::test2', src_dir)
  346. output = self.attic('prune', '-v', '--dry-run', self.repository_location, '--keep-daily=2')
  347. self.assert_in('Keeping archive: test2', output)
  348. self.assert_in('Would prune: test1', output)
  349. output = self.attic('list', self.repository_location)
  350. self.assert_in('test1', output)
  351. self.assert_in('test2', output)
  352. self.attic('prune', self.repository_location, '--keep-daily=2')
  353. output = self.attic('list', self.repository_location)
  354. self.assert_not_in('test1', output)
  355. self.assert_in('test2', output)
  356. def test_usage(self):
  357. self.assert_raises(SystemExit, lambda: self.attic())
  358. self.assert_raises(SystemExit, lambda: self.attic('-h'))
  359. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  360. def test_fuse_mount_repository(self):
  361. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  362. os.mkdir(mountpoint)
  363. self.attic('init', self.repository_location)
  364. self.create_test_files()
  365. self.attic('create', self.repository_location + '::archive', 'input')
  366. self.attic('create', self.repository_location + '::archive2', 'input')
  367. try:
  368. self.attic('mount', self.repository_location, mountpoint, fork=True)
  369. self.wait_for_mount(mountpoint)
  370. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive', 'input'))
  371. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive2', 'input'))
  372. finally:
  373. if sys.platform.startswith('linux'):
  374. os.system('fusermount -u ' + mountpoint)
  375. else:
  376. os.system('umount ' + mountpoint)
  377. os.rmdir(mountpoint)
  378. # Give the daemon some time to exit
  379. time.sleep(.2)
  380. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  381. def test_fuse_mount_archive(self):
  382. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  383. os.mkdir(mountpoint)
  384. self.attic('init', self.repository_location)
  385. self.create_test_files()
  386. self.attic('create', self.repository_location + '::archive', 'input')
  387. try:
  388. self.attic('mount', self.repository_location + '::archive', mountpoint, fork=True)
  389. self.wait_for_mount(mountpoint)
  390. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'input'))
  391. finally:
  392. if sys.platform.startswith('linux'):
  393. os.system('fusermount -u ' + mountpoint)
  394. else:
  395. os.system('umount ' + mountpoint)
  396. os.rmdir(mountpoint)
  397. # Give the daemon some time to exit
  398. time.sleep(.2)
  399. def verify_aes_counter_uniqueness(self, method):
  400. seen = set() # Chunks already seen
  401. used = set() # counter values already used
  402. def verify_uniqueness():
  403. repository = Repository(self.repository_path)
  404. for key, _ in repository.open_index(repository.get_transaction_id()).iteritems():
  405. data = repository.get(key)
  406. hash = sha256(data).digest()
  407. if hash not in seen:
  408. seen.add(hash)
  409. mac, meta, data = parser(data)
  410. num_blocks = num_aes_blocks(len(data))
  411. nonce = bytes16_to_int(meta.iv)
  412. for counter in range(nonce, nonce + num_blocks):
  413. self.assert_not_in(counter, used)
  414. used.add(counter)
  415. self.create_test_files()
  416. os.environ['ATTIC_PASSPHRASE'] = 'passphrase'
  417. self.attic('init', '--encryption=' + method, self.repository_location)
  418. verify_uniqueness()
  419. self.attic('create', self.repository_location + '::test', 'input')
  420. verify_uniqueness()
  421. self.attic('create', self.repository_location + '::test.2', 'input')
  422. verify_uniqueness()
  423. self.attic('delete', self.repository_location + '::test.2')
  424. verify_uniqueness()
  425. self.assert_equal(used, set(range(len(used))))
  426. def test_aes_counter_uniqueness_keyfile(self):
  427. self.verify_aes_counter_uniqueness('keyfile')
  428. def test_aes_counter_uniqueness_passphrase(self):
  429. self.verify_aes_counter_uniqueness('passphrase')
  430. class ArchiverCheckTestCase(ArchiverTestCaseBase):
  431. def setUp(self):
  432. super(ArchiverCheckTestCase, self).setUp()
  433. with patch.object(ChunkBuffer, 'BUFFER_SIZE', 10):
  434. self.attic('init', self.repository_location)
  435. self.create_src_archive('archive1')
  436. self.create_src_archive('archive2')
  437. def open_archive(self, name):
  438. repository = Repository(self.repository_path)
  439. manifest, key = Manifest.load(repository)
  440. archive = Archive(repository, key, manifest, name)
  441. return archive, repository
  442. def test_check_usage(self):
  443. output = self.attic('check', self.repository_location, exit_code=0)
  444. self.assert_in('Starting repository check', output)
  445. self.assert_in('Starting archive consistency check', output)
  446. output = self.attic('check', '--repository-only', self.repository_location, exit_code=0)
  447. self.assert_in('Starting repository check', output)
  448. self.assert_not_in('Starting archive consistency check', output)
  449. output = self.attic('check', '--archives-only', self.repository_location, exit_code=0)
  450. self.assert_not_in('Starting repository check', output)
  451. self.assert_in('Starting archive consistency check', output)
  452. def test_missing_file_chunk(self):
  453. archive, repository = self.open_archive('archive1')
  454. for item in archive.iter_items():
  455. if item[b'path'].endswith('testsuite/archiver.py'):
  456. repository.delete(item[b'chunks'][-1][0])
  457. break
  458. repository.commit()
  459. self.attic('check', self.repository_location, exit_code=1)
  460. self.attic('check', '--repair', self.repository_location, exit_code=0)
  461. self.attic('check', self.repository_location, exit_code=0)
  462. def test_missing_archive_item_chunk(self):
  463. archive, repository = self.open_archive('archive1')
  464. repository.delete(archive.metadata[b'items'][-5])
  465. repository.commit()
  466. self.attic('check', self.repository_location, exit_code=1)
  467. self.attic('check', '--repair', self.repository_location, exit_code=0)
  468. self.attic('check', self.repository_location, exit_code=0)
  469. def test_missing_archive_metadata(self):
  470. archive, repository = self.open_archive('archive1')
  471. repository.delete(archive.id)
  472. repository.commit()
  473. self.attic('check', self.repository_location, exit_code=1)
  474. self.attic('check', '--repair', self.repository_location, exit_code=0)
  475. self.attic('check', self.repository_location, exit_code=0)
  476. def test_missing_manifest(self):
  477. archive, repository = self.open_archive('archive1')
  478. repository.delete(Manifest.manifest_id(repository))
  479. repository.commit()
  480. self.attic('check', self.repository_location, exit_code=1)
  481. output = self.attic('check', '--repair', self.repository_location, exit_code=0)
  482. self.assert_in('archive1', output)
  483. self.assert_in('archive2', output)
  484. self.attic('check', self.repository_location, exit_code=0)
  485. def test_extra_chunks(self):
  486. self.attic('check', self.repository_location, exit_code=0)
  487. repository = Repository(self.repository_location)
  488. repository.put(b'0123456789012345', b'xxxx')
  489. repository.commit()
  490. repository.close()
  491. self.attic('check', self.repository_location, exit_code=1)
  492. self.attic('check', self.repository_location, exit_code=1)
  493. self.attic('check', '--repair', self.repository_location, exit_code=0)
  494. self.attic('check', self.repository_location, exit_code=0)
  495. self.attic('extract', '--dry-run', self.repository_location + '::archive1', exit_code=0)
  496. class RemoteArchiverTestCase(ArchiverTestCase):
  497. prefix = '__testsuite__:'
  498. def test_remote_repo_restrict_to_path(self):
  499. self.attic('init', self.repository_location)
  500. path_prefix = os.path.dirname(self.repository_path)
  501. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo']):
  502. self.assert_raises(PathNotAllowed, lambda: self.attic('init', self.repository_location + '_1'))
  503. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', path_prefix]):
  504. self.attic('init', self.repository_location + '_2')
  505. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]):
  506. self.attic('init', self.repository_location + '_3')