archiver.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  1. from binascii import hexlify
  2. from configparser import RawConfigParser
  3. import errno
  4. import os
  5. from io import StringIO
  6. import stat
  7. import subprocess
  8. import sys
  9. import shutil
  10. import tempfile
  11. import time
  12. import unittest
  13. from hashlib import sha256
  14. from mock import patch
  15. import pytest
  16. from .. import xattr
  17. from ..archive import Archive, ChunkBuffer, CHUNK_MAX_EXP
  18. from ..archiver import Archiver
  19. from ..cache import Cache
  20. from ..crypto import bytes_to_long, num_aes_blocks
  21. from ..helpers import Manifest, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, st_atime_ns, st_mtime_ns
  22. from ..remote import RemoteRepository, PathNotAllowed
  23. from ..repository import Repository
  24. from . import BaseTestCase
  25. try:
  26. import llfuse
  27. has_llfuse = True or llfuse # avoids "unused import"
  28. except ImportError:
  29. has_llfuse = False
  30. has_lchflags = hasattr(os, 'lchflags')
  31. src_dir = os.path.join(os.getcwd(), os.path.dirname(__file__), '..')
  32. # Python <= 3.2 raises OSError instead of PermissionError (See #164)
  33. try:
  34. PermissionError = PermissionError
  35. except NameError:
  36. PermissionError = OSError
  37. class changedir:
  38. def __init__(self, dir):
  39. self.dir = dir
  40. def __enter__(self):
  41. self.old = os.getcwd()
  42. os.chdir(self.dir)
  43. def __exit__(self, *args, **kw):
  44. os.chdir(self.old)
  45. class environment_variable:
  46. def __init__(self, **values):
  47. self.values = values
  48. self.old_values = {}
  49. def __enter__(self):
  50. for k, v in self.values.items():
  51. self.old_values[k] = os.environ.get(k)
  52. os.environ[k] = v
  53. def __exit__(self, *args, **kw):
  54. for k, v in self.old_values.items():
  55. if v is None:
  56. del os.environ[k]
  57. else:
  58. os.environ[k] = v
  59. def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw):
  60. if fork:
  61. try:
  62. if exe is None:
  63. borg = (sys.executable, '-m', 'borg.archiver')
  64. elif isinstance(exe, str):
  65. borg = (exe, )
  66. elif not isinstance(exe, tuple):
  67. raise ValueError('exe must be None, a tuple or a str')
  68. output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT)
  69. ret = 0
  70. except subprocess.CalledProcessError as e:
  71. output = e.output
  72. ret = e.returncode
  73. return ret, os.fsdecode(output)
  74. else:
  75. stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr
  76. try:
  77. sys.stdin = StringIO()
  78. sys.stdout = sys.stderr = output = StringIO()
  79. if archiver is None:
  80. archiver = Archiver()
  81. ret = archiver.run(list(args))
  82. return ret, output.getvalue()
  83. finally:
  84. sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr
  85. # check if the binary "borg.exe" is available
  86. try:
  87. exec_cmd('help', exe='borg.exe', fork=True)
  88. BORG_EXES = ['python', 'binary', ]
  89. except (IOError, OSError) as err:
  90. if err.errno != errno.ENOENT:
  91. raise
  92. BORG_EXES = ['python', ]
  93. @pytest.fixture(params=BORG_EXES)
  94. def cmd(request):
  95. if request.param == 'python':
  96. exe = None
  97. elif request.param == 'binary':
  98. exe = 'borg.exe'
  99. else:
  100. raise ValueError("param must be 'python' or 'binary'")
  101. def exec_fn(*args, **kw):
  102. return exec_cmd(*args, exe=exe, fork=True, **kw)
  103. return exec_fn
  104. def test_return_codes(cmd, tmpdir):
  105. repo = tmpdir.mkdir('repo')
  106. input = tmpdir.mkdir('input')
  107. output = tmpdir.mkdir('output')
  108. input.join('test_file').write('content')
  109. rc, out = cmd('init', '%s' % str(repo))
  110. assert rc == EXIT_SUCCESS
  111. rc, out = cmd('create', '%s::archive' % repo, str(input))
  112. assert rc == EXIT_SUCCESS
  113. with changedir(str(output)):
  114. rc, out = cmd('extract', '%s::archive' % repo)
  115. assert rc == EXIT_SUCCESS
  116. rc, out = cmd('extract', '%s::archive' % repo, 'does/not/match')
  117. assert rc == EXIT_WARNING # pattern did not match
  118. rc, out = cmd('create', '%s::archive' % repo, str(input))
  119. assert rc == EXIT_ERROR # duplicate archive name
  120. class ArchiverTestCaseBase(BaseTestCase):
  121. EXE = None # python source based
  122. FORK_DEFAULT = False
  123. prefix = ''
  124. def setUp(self):
  125. os.environ['BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'] = '1'
  126. self.archiver = not self.FORK_DEFAULT and Archiver() or None
  127. self.tmpdir = tempfile.mkdtemp()
  128. self.repository_path = os.path.join(self.tmpdir, 'repository')
  129. self.repository_location = self.prefix + self.repository_path
  130. self.input_path = os.path.join(self.tmpdir, 'input')
  131. self.output_path = os.path.join(self.tmpdir, 'output')
  132. self.keys_path = os.path.join(self.tmpdir, 'keys')
  133. self.cache_path = os.path.join(self.tmpdir, 'cache')
  134. self.exclude_file_path = os.path.join(self.tmpdir, 'excludes')
  135. os.environ['BORG_KEYS_DIR'] = self.keys_path
  136. os.environ['BORG_CACHE_DIR'] = self.cache_path
  137. os.mkdir(self.input_path)
  138. os.mkdir(self.output_path)
  139. os.mkdir(self.keys_path)
  140. os.mkdir(self.cache_path)
  141. with open(self.exclude_file_path, 'wb') as fd:
  142. fd.write(b'input/file2\n# A comment line, then a blank line\n\n')
  143. self._old_wd = os.getcwd()
  144. os.chdir(self.tmpdir)
  145. def tearDown(self):
  146. os.chdir(self._old_wd)
  147. shutil.rmtree(self.tmpdir)
  148. def cmd(self, *args, **kw):
  149. exit_code = kw.pop('exit_code', 0)
  150. fork = kw.pop('fork', None)
  151. if fork is None:
  152. fork = self.FORK_DEFAULT
  153. ret, output = exec_cmd(*args, fork=fork, exe=self.EXE, archiver=self.archiver, **kw)
  154. if ret != exit_code:
  155. print(output)
  156. self.assert_equal(ret, exit_code)
  157. return output
  158. def create_src_archive(self, name):
  159. self.cmd('create', self.repository_location + '::' + name, src_dir)
  160. class ArchiverTestCase(ArchiverTestCaseBase):
  161. def create_regular_file(self, name, size=0, contents=None):
  162. filename = os.path.join(self.input_path, name)
  163. if not os.path.exists(os.path.dirname(filename)):
  164. os.makedirs(os.path.dirname(filename))
  165. with open(filename, 'wb') as fd:
  166. if contents is None:
  167. contents = b'X' * size
  168. fd.write(contents)
  169. def create_test_files(self):
  170. """Create a minimal test case including all supported file types
  171. """
  172. # File
  173. self.create_regular_file('empty', size=0)
  174. # next code line raises OverflowError on 32bit cpu (raspberry pi 2):
  175. # 2600-01-01 > 2**64 ns
  176. # os.utime('input/empty', (19880895600, 19880895600))
  177. # thus, we better test with something not that far in future:
  178. # 2038-01-19 (1970 + 2^31 - 1 seconds) is the 32bit "deadline":
  179. os.utime('input/empty', (2**31 - 1, 2**31 - 1))
  180. self.create_regular_file('file1', size=1024 * 80)
  181. self.create_regular_file('flagfile', size=1024)
  182. # Directory
  183. self.create_regular_file('dir2/file2', size=1024 * 80)
  184. # File mode
  185. os.chmod('input/file1', 0o4755)
  186. # Hard link
  187. os.link(os.path.join(self.input_path, 'file1'),
  188. os.path.join(self.input_path, 'hardlink'))
  189. # Symlink
  190. os.symlink('somewhere', os.path.join(self.input_path, 'link1'))
  191. if xattr.is_enabled(self.input_path):
  192. xattr.setxattr(os.path.join(self.input_path, 'file1'), 'user.foo', b'bar')
  193. # XXX this always fails for me
  194. # ubuntu 14.04, on a TMP dir filesystem with user_xattr, using fakeroot
  195. # same for newer ubuntu and centos.
  196. # if this is supported just on specific platform, platform should be checked first,
  197. # so that the test setup for all tests using it does not fail here always for others.
  198. # xattr.setxattr(os.path.join(self.input_path, 'link1'), 'user.foo_symlink', b'bar_symlink', follow_symlinks=False)
  199. # FIFO node
  200. os.mkfifo(os.path.join(self.input_path, 'fifo1'))
  201. if has_lchflags:
  202. os.lchflags(os.path.join(self.input_path, 'flagfile'), stat.UF_NODUMP)
  203. try:
  204. # Block device
  205. os.mknod('input/bdev', 0o600 | stat.S_IFBLK, os.makedev(10, 20))
  206. # Char device
  207. os.mknod('input/cdev', 0o600 | stat.S_IFCHR, os.makedev(30, 40))
  208. # File mode
  209. os.chmod('input/dir2', 0o555) # if we take away write perms, we need root to remove contents
  210. # File owner
  211. os.chown('input/file1', 100, 200)
  212. have_root = True # we have (fake)root
  213. except PermissionError:
  214. have_root = False
  215. return have_root
  216. def test_basic_functionality(self):
  217. have_root = self.create_test_files()
  218. self.cmd('init', self.repository_location)
  219. self.cmd('create', self.repository_location + '::test', 'input')
  220. self.cmd('create', '--stats', self.repository_location + '::test.2', 'input')
  221. with changedir('output'):
  222. self.cmd('extract', self.repository_location + '::test')
  223. self.assert_equal(len(self.cmd('list', self.repository_location).splitlines()), 2)
  224. expected = [
  225. 'input',
  226. 'input/bdev',
  227. 'input/cdev',
  228. 'input/dir2',
  229. 'input/dir2/file2',
  230. 'input/empty',
  231. 'input/fifo1',
  232. 'input/file1',
  233. 'input/flagfile',
  234. 'input/hardlink',
  235. 'input/link1',
  236. ]
  237. if not have_root:
  238. # we could not create these device files without (fake)root
  239. expected.remove('input/bdev')
  240. expected.remove('input/cdev')
  241. if has_lchflags:
  242. # remove the file we did not backup, so input and output become equal
  243. expected.remove('input/flagfile') # this file is UF_NODUMP
  244. os.remove(os.path.join('input', 'flagfile'))
  245. self.assert_equal(self.cmd('list', '--short', self.repository_location + '::test').splitlines(), expected)
  246. self.assert_dirs_equal('input', 'output/input')
  247. info_output = self.cmd('info', self.repository_location + '::test')
  248. item_count = 3 if has_lchflags else 4 # one file is UF_NODUMP
  249. self.assert_in('Number of files: %d' % item_count, info_output)
  250. shutil.rmtree(self.cache_path)
  251. with environment_variable(BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='1'):
  252. info_output2 = self.cmd('info', self.repository_location + '::test')
  253. # info_output2 starts with some "initializing cache" text but should
  254. # end the same way as info_output
  255. assert info_output2.endswith(info_output)
  256. def test_atime(self):
  257. have_root = self.create_test_files()
  258. atime, mtime = 123456780, 234567890
  259. os.utime('input/file1', (atime, mtime))
  260. self.cmd('init', self.repository_location)
  261. self.cmd('create', self.repository_location + '::test', 'input')
  262. with changedir('output'):
  263. self.cmd('extract', self.repository_location + '::test')
  264. sti = os.stat('input/file1')
  265. sto = os.stat('output/input/file1')
  266. assert st_mtime_ns(sti) == st_mtime_ns(sto) == mtime * 1e9
  267. assert st_atime_ns(sti) == st_atime_ns(sto) == atime * 1e9
  268. def _extract_repository_id(self, path):
  269. return Repository(self.repository_path).id
  270. def _set_repository_id(self, path, id):
  271. config = RawConfigParser()
  272. config.read(os.path.join(path, 'config'))
  273. config.set('repository', 'id', hexlify(id).decode('ascii'))
  274. with open(os.path.join(path, 'config'), 'w') as fd:
  275. config.write(fd)
  276. return Repository(self.repository_path).id
  277. def test_sparse_file(self):
  278. # no sparse file support on Mac OS X
  279. sparse_support = sys.platform != 'darwin'
  280. filename = os.path.join(self.input_path, 'sparse')
  281. content = b'foobar'
  282. hole_size = 5 * (1 << CHUNK_MAX_EXP) # 5 full chunker buffers
  283. with open(filename, 'wb') as fd:
  284. # create a file that has a hole at the beginning and end (if the
  285. # OS and filesystem supports sparse files)
  286. fd.seek(hole_size, 1)
  287. fd.write(content)
  288. fd.seek(hole_size, 1)
  289. pos = fd.tell()
  290. fd.truncate(pos)
  291. total_len = hole_size + len(content) + hole_size
  292. st = os.stat(filename)
  293. self.assert_equal(st.st_size, total_len)
  294. if sparse_support and hasattr(st, 'st_blocks'):
  295. self.assert_true(st.st_blocks * 512 < total_len / 9) # is input sparse?
  296. self.cmd('init', self.repository_location)
  297. self.cmd('create', self.repository_location + '::test', 'input')
  298. with changedir('output'):
  299. self.cmd('extract', '--sparse', self.repository_location + '::test')
  300. self.assert_dirs_equal('input', 'output/input')
  301. filename = os.path.join(self.output_path, 'input', 'sparse')
  302. with open(filename, 'rb') as fd:
  303. # check if file contents are as expected
  304. self.assert_equal(fd.read(hole_size), b'\0' * hole_size)
  305. self.assert_equal(fd.read(len(content)), content)
  306. self.assert_equal(fd.read(hole_size), b'\0' * hole_size)
  307. st = os.stat(filename)
  308. self.assert_equal(st.st_size, total_len)
  309. if sparse_support and hasattr(st, 'st_blocks'):
  310. self.assert_true(st.st_blocks * 512 < total_len / 9) # is output sparse?
  311. def test_unusual_filenames(self):
  312. filenames = ['normal', 'with some blanks', '(with_parens)', ]
  313. for filename in filenames:
  314. filename = os.path.join(self.input_path, filename)
  315. with open(filename, 'wb') as fd:
  316. pass
  317. self.cmd('init', self.repository_location)
  318. self.cmd('create', self.repository_location + '::test', 'input')
  319. for filename in filenames:
  320. with changedir('output'):
  321. self.cmd('extract', self.repository_location + '::test', os.path.join('input', filename))
  322. assert os.path.exists(os.path.join('output', 'input', filename))
  323. def test_repository_swap_detection(self):
  324. self.create_test_files()
  325. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  326. self.cmd('init', '--encryption=passphrase', self.repository_location)
  327. repository_id = self._extract_repository_id(self.repository_path)
  328. self.cmd('create', self.repository_location + '::test', 'input')
  329. shutil.rmtree(self.repository_path)
  330. self.cmd('init', '--encryption=none', self.repository_location)
  331. self._set_repository_id(self.repository_path, repository_id)
  332. self.assert_equal(repository_id, self._extract_repository_id(self.repository_path))
  333. if self.FORK_DEFAULT:
  334. self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=1) # fails
  335. else:
  336. self.assert_raises(Cache.EncryptionMethodMismatch, lambda: self.cmd('create', self.repository_location + '::test.2', 'input'))
  337. def test_repository_swap_detection2(self):
  338. self.create_test_files()
  339. self.cmd('init', '--encryption=none', self.repository_location + '_unencrypted')
  340. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  341. self.cmd('init', '--encryption=passphrase', self.repository_location + '_encrypted')
  342. self.cmd('create', self.repository_location + '_encrypted::test', 'input')
  343. shutil.rmtree(self.repository_path + '_encrypted')
  344. os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted')
  345. if self.FORK_DEFAULT:
  346. self.cmd('create', self.repository_location + '_encrypted::test.2', 'input', exit_code=1) # fails
  347. else:
  348. self.assert_raises(Cache.RepositoryAccessAborted, lambda: self.cmd('create', self.repository_location + '_encrypted::test.2', 'input'))
  349. def test_strip_components(self):
  350. self.cmd('init', self.repository_location)
  351. self.create_regular_file('dir/file')
  352. self.cmd('create', self.repository_location + '::test', 'input')
  353. with changedir('output'):
  354. self.cmd('extract', self.repository_location + '::test', '--strip-components', '3')
  355. self.assert_true(not os.path.exists('file'))
  356. with self.assert_creates_file('file'):
  357. self.cmd('extract', self.repository_location + '::test', '--strip-components', '2')
  358. with self.assert_creates_file('dir/file'):
  359. self.cmd('extract', self.repository_location + '::test', '--strip-components', '1')
  360. with self.assert_creates_file('input/dir/file'):
  361. self.cmd('extract', self.repository_location + '::test', '--strip-components', '0')
  362. def test_extract_include_exclude(self):
  363. self.cmd('init', self.repository_location)
  364. self.create_regular_file('file1', size=1024 * 80)
  365. self.create_regular_file('file2', size=1024 * 80)
  366. self.create_regular_file('file3', size=1024 * 80)
  367. self.create_regular_file('file4', size=1024 * 80)
  368. self.cmd('create', '--exclude=input/file4', self.repository_location + '::test', 'input')
  369. with changedir('output'):
  370. self.cmd('extract', self.repository_location + '::test', 'input/file1', )
  371. self.assert_equal(sorted(os.listdir('output/input')), ['file1'])
  372. with changedir('output'):
  373. self.cmd('extract', '--exclude=input/file2', self.repository_location + '::test')
  374. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  375. with changedir('output'):
  376. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  377. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  378. def test_exclude_caches(self):
  379. self.cmd('init', self.repository_location)
  380. self.create_regular_file('file1', size=1024 * 80)
  381. self.create_regular_file('cache1/CACHEDIR.TAG', contents=b'Signature: 8a477f597d28d172789f06886806bc55 extra stuff')
  382. self.create_regular_file('cache2/CACHEDIR.TAG', contents=b'invalid signature')
  383. self.cmd('create', '--exclude-caches', self.repository_location + '::test', 'input')
  384. with changedir('output'):
  385. self.cmd('extract', self.repository_location + '::test')
  386. self.assert_equal(sorted(os.listdir('output/input')), ['cache2', 'file1'])
  387. self.assert_equal(sorted(os.listdir('output/input/cache2')), ['CACHEDIR.TAG'])
  388. def test_path_normalization(self):
  389. self.cmd('init', self.repository_location)
  390. self.create_regular_file('dir1/dir2/file', size=1024 * 80)
  391. with changedir('input/dir1/dir2'):
  392. self.cmd('create', self.repository_location + '::test', '../../../input/dir1/../dir1/dir2/..')
  393. output = self.cmd('list', self.repository_location + '::test')
  394. self.assert_not_in('..', output)
  395. self.assert_in(' input/dir1/dir2/file', output)
  396. def test_exclude_normalization(self):
  397. self.cmd('init', self.repository_location)
  398. self.create_regular_file('file1', size=1024 * 80)
  399. self.create_regular_file('file2', size=1024 * 80)
  400. with changedir('input'):
  401. self.cmd('create', '--exclude=file1', self.repository_location + '::test1', '.')
  402. with changedir('output'):
  403. self.cmd('extract', self.repository_location + '::test1')
  404. self.assert_equal(sorted(os.listdir('output')), ['file2'])
  405. with changedir('input'):
  406. self.cmd('create', '--exclude=./file1', self.repository_location + '::test2', '.')
  407. with changedir('output'):
  408. self.cmd('extract', self.repository_location + '::test2')
  409. self.assert_equal(sorted(os.listdir('output')), ['file2'])
  410. self.cmd('create', '--exclude=input/./file1', self.repository_location + '::test3', 'input')
  411. with changedir('output'):
  412. self.cmd('extract', self.repository_location + '::test3')
  413. self.assert_equal(sorted(os.listdir('output/input')), ['file2'])
  414. def test_repeated_files(self):
  415. self.create_regular_file('file1', size=1024 * 80)
  416. self.cmd('init', self.repository_location)
  417. self.cmd('create', self.repository_location + '::test', 'input', 'input')
  418. def test_overwrite(self):
  419. self.create_regular_file('file1', size=1024 * 80)
  420. self.create_regular_file('dir2/file2', size=1024 * 80)
  421. self.cmd('init', self.repository_location)
  422. self.cmd('create', self.repository_location + '::test', 'input')
  423. # Overwriting regular files and directories should be supported
  424. os.mkdir('output/input')
  425. os.mkdir('output/input/file1')
  426. os.mkdir('output/input/dir2')
  427. with changedir('output'):
  428. self.cmd('extract', self.repository_location + '::test')
  429. self.assert_dirs_equal('input', 'output/input')
  430. # But non-empty dirs should fail
  431. os.unlink('output/input/file1')
  432. os.mkdir('output/input/file1')
  433. os.mkdir('output/input/file1/dir')
  434. with changedir('output'):
  435. self.cmd('extract', self.repository_location + '::test', exit_code=1)
  436. def test_rename(self):
  437. self.create_regular_file('file1', size=1024 * 80)
  438. self.create_regular_file('dir2/file2', size=1024 * 80)
  439. self.cmd('init', self.repository_location)
  440. self.cmd('create', self.repository_location + '::test', 'input')
  441. self.cmd('create', self.repository_location + '::test.2', 'input')
  442. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  443. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  444. self.cmd('rename', self.repository_location + '::test', 'test.3')
  445. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  446. self.cmd('rename', self.repository_location + '::test.2', 'test.4')
  447. self.cmd('extract', '--dry-run', self.repository_location + '::test.3')
  448. self.cmd('extract', '--dry-run', self.repository_location + '::test.4')
  449. # Make sure both archives have been renamed
  450. repository = Repository(self.repository_path)
  451. manifest, key = Manifest.load(repository)
  452. self.assert_equal(len(manifest.archives), 2)
  453. self.assert_in('test.3', manifest.archives)
  454. self.assert_in('test.4', manifest.archives)
  455. def test_delete(self):
  456. self.create_regular_file('file1', size=1024 * 80)
  457. self.create_regular_file('dir2/file2', size=1024 * 80)
  458. self.cmd('init', self.repository_location)
  459. self.cmd('create', self.repository_location + '::test', 'input')
  460. self.cmd('create', self.repository_location + '::test.2', 'input')
  461. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  462. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  463. self.cmd('delete', self.repository_location + '::test')
  464. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  465. self.cmd('delete', '--stats', self.repository_location + '::test.2')
  466. # Make sure all data except the manifest has been deleted
  467. repository = Repository(self.repository_path)
  468. self.assert_equal(len(repository), 1)
  469. def test_delete_repo(self):
  470. self.create_regular_file('file1', size=1024 * 80)
  471. self.create_regular_file('dir2/file2', size=1024 * 80)
  472. self.cmd('init', self.repository_location)
  473. self.cmd('create', self.repository_location + '::test', 'input')
  474. self.cmd('create', self.repository_location + '::test.2', 'input')
  475. self.cmd('delete', self.repository_location)
  476. # Make sure the repo is gone
  477. self.assertFalse(os.path.exists(self.repository_path))
  478. def test_corrupted_repository(self):
  479. self.cmd('init', self.repository_location)
  480. self.create_src_archive('test')
  481. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  482. self.cmd('check', self.repository_location)
  483. name = sorted(os.listdir(os.path.join(self.tmpdir, 'repository', 'data', '0')), reverse=True)[0]
  484. with open(os.path.join(self.tmpdir, 'repository', 'data', '0', name), 'r+b') as fd:
  485. fd.seek(100)
  486. fd.write(b'XXXX')
  487. self.cmd('check', self.repository_location, exit_code=1)
  488. # we currently need to be able to create a lock directory inside the repo:
  489. @pytest.mark.xfail(reason="we need to be able to create the lock directory inside the repo")
  490. def test_readonly_repository(self):
  491. self.cmd('init', self.repository_location)
  492. self.create_src_archive('test')
  493. os.system('chmod -R ugo-w ' + self.repository_path)
  494. try:
  495. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  496. finally:
  497. # Restore permissions so shutil.rmtree is able to delete it
  498. os.system('chmod -R u+w ' + self.repository_path)
  499. def test_umask(self):
  500. self.create_regular_file('file1', size=1024 * 80)
  501. self.cmd('init', self.repository_location)
  502. self.cmd('create', self.repository_location + '::test', 'input')
  503. mode = os.stat(self.repository_path).st_mode
  504. self.assertEqual(stat.S_IMODE(mode), 0o700)
  505. def test_create_dry_run(self):
  506. self.cmd('init', self.repository_location)
  507. self.cmd('create', '--dry-run', self.repository_location + '::test', 'input')
  508. # Make sure no archive has been created
  509. repository = Repository(self.repository_path)
  510. manifest, key = Manifest.load(repository)
  511. self.assert_equal(len(manifest.archives), 0)
  512. def test_cmdline_compatibility(self):
  513. self.create_regular_file('file1', size=1024 * 80)
  514. self.cmd('init', self.repository_location)
  515. self.cmd('create', self.repository_location + '::test', 'input')
  516. output = self.cmd('verify', '-v', self.repository_location + '::test')
  517. self.assert_in('"borg verify" has been deprecated', output)
  518. output = self.cmd('prune', self.repository_location, '--hourly=1')
  519. self.assert_in('"--hourly" has been deprecated. Use "--keep-hourly" instead', output)
  520. def test_prune_repository(self):
  521. self.cmd('init', self.repository_location)
  522. self.cmd('create', self.repository_location + '::test1', src_dir)
  523. self.cmd('create', self.repository_location + '::test2', src_dir)
  524. output = self.cmd('prune', '-v', '--dry-run', self.repository_location, '--keep-daily=2')
  525. self.assert_in('Keeping archive: test2', output)
  526. self.assert_in('Would prune: test1', output)
  527. output = self.cmd('list', self.repository_location)
  528. self.assert_in('test1', output)
  529. self.assert_in('test2', output)
  530. self.cmd('prune', self.repository_location, '--keep-daily=2')
  531. output = self.cmd('list', self.repository_location)
  532. self.assert_not_in('test1', output)
  533. self.assert_in('test2', output)
  534. def test_prune_repository_prefix(self):
  535. self.cmd('init', self.repository_location)
  536. self.cmd('create', self.repository_location + '::foo-2015-08-12-10:00', src_dir)
  537. self.cmd('create', self.repository_location + '::foo-2015-08-12-20:00', src_dir)
  538. self.cmd('create', self.repository_location + '::bar-2015-08-12-10:00', src_dir)
  539. self.cmd('create', self.repository_location + '::bar-2015-08-12-20:00', src_dir)
  540. output = self.cmd('prune', '-v', '--dry-run', self.repository_location, '--keep-daily=2', '--prefix=foo-')
  541. self.assert_in('Keeping archive: foo-2015-08-12-20:00', output)
  542. self.assert_in('Would prune: foo-2015-08-12-10:00', output)
  543. output = self.cmd('list', self.repository_location)
  544. self.assert_in('foo-2015-08-12-10:00', output)
  545. self.assert_in('foo-2015-08-12-20:00', output)
  546. self.assert_in('bar-2015-08-12-10:00', output)
  547. self.assert_in('bar-2015-08-12-20:00', output)
  548. self.cmd('prune', self.repository_location, '--keep-daily=2', '--prefix=foo-')
  549. output = self.cmd('list', self.repository_location)
  550. self.assert_not_in('foo-2015-08-12-10:00', output)
  551. self.assert_in('foo-2015-08-12-20:00', output)
  552. self.assert_in('bar-2015-08-12-10:00', output)
  553. self.assert_in('bar-2015-08-12-20:00', output)
  554. def test_usage(self):
  555. if self.FORK_DEFAULT:
  556. self.cmd(exit_code=0)
  557. self.cmd('-h', exit_code=0)
  558. else:
  559. self.assert_raises(SystemExit, lambda: self.cmd())
  560. self.assert_raises(SystemExit, lambda: self.cmd('-h'))
  561. def test_help(self):
  562. assert 'Borg' in self.cmd('help')
  563. assert 'patterns' in self.cmd('help', 'patterns')
  564. assert 'Initialize' in self.cmd('help', 'init')
  565. assert 'positional arguments' not in self.cmd('help', 'init', '--epilog-only')
  566. assert 'This command initializes' not in self.cmd('help', 'init', '--usage-only')
  567. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  568. def test_fuse_mount_repository(self):
  569. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  570. os.mkdir(mountpoint)
  571. self.cmd('init', self.repository_location)
  572. self.create_test_files()
  573. self.cmd('create', self.repository_location + '::archive', 'input')
  574. self.cmd('create', self.repository_location + '::archive2', 'input')
  575. try:
  576. self.cmd('mount', self.repository_location, mountpoint, fork=True)
  577. self.wait_for_mount(mountpoint)
  578. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive', 'input'))
  579. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive2', 'input'))
  580. finally:
  581. if sys.platform.startswith('linux'):
  582. os.system('fusermount -u ' + mountpoint)
  583. else:
  584. os.system('umount ' + mountpoint)
  585. os.rmdir(mountpoint)
  586. # Give the daemon some time to exit
  587. time.sleep(.2)
  588. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  589. def test_fuse_mount_archive(self):
  590. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  591. os.mkdir(mountpoint)
  592. self.cmd('init', self.repository_location)
  593. self.create_test_files()
  594. self.cmd('create', self.repository_location + '::archive', 'input')
  595. try:
  596. self.cmd('mount', self.repository_location + '::archive', mountpoint, fork=True)
  597. self.wait_for_mount(mountpoint)
  598. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'input'))
  599. finally:
  600. if sys.platform.startswith('linux'):
  601. os.system('fusermount -u ' + mountpoint)
  602. else:
  603. os.system('umount ' + mountpoint)
  604. os.rmdir(mountpoint)
  605. # Give the daemon some time to exit
  606. time.sleep(.2)
  607. def verify_aes_counter_uniqueness(self, method):
  608. seen = set() # Chunks already seen
  609. used = set() # counter values already used
  610. def verify_uniqueness():
  611. repository = Repository(self.repository_path)
  612. for key, _ in repository.open_index(repository.get_transaction_id()).iteritems():
  613. data = repository.get(key)
  614. hash = sha256(data).digest()
  615. if hash not in seen:
  616. seen.add(hash)
  617. num_blocks = num_aes_blocks(len(data) - 41)
  618. nonce = bytes_to_long(data[33:41])
  619. for counter in range(nonce, nonce + num_blocks):
  620. self.assert_not_in(counter, used)
  621. used.add(counter)
  622. self.create_test_files()
  623. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  624. self.cmd('init', '--encryption=' + method, self.repository_location)
  625. verify_uniqueness()
  626. self.cmd('create', self.repository_location + '::test', 'input')
  627. verify_uniqueness()
  628. self.cmd('create', self.repository_location + '::test.2', 'input')
  629. verify_uniqueness()
  630. self.cmd('delete', self.repository_location + '::test.2')
  631. verify_uniqueness()
  632. self.assert_equal(used, set(range(len(used))))
  633. def test_aes_counter_uniqueness_keyfile(self):
  634. self.verify_aes_counter_uniqueness('keyfile')
  635. def test_aes_counter_uniqueness_passphrase(self):
  636. self.verify_aes_counter_uniqueness('passphrase')
  637. @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
  638. class ArchiverTestCaseBinary(ArchiverTestCase):
  639. EXE = 'borg.exe'
  640. FORK_DEFAULT = True
  641. class ArchiverCheckTestCase(ArchiverTestCaseBase):
  642. def setUp(self):
  643. super().setUp()
  644. with patch.object(ChunkBuffer, 'BUFFER_SIZE', 10):
  645. self.cmd('init', self.repository_location)
  646. self.create_src_archive('archive1')
  647. self.create_src_archive('archive2')
  648. def open_archive(self, name):
  649. repository = Repository(self.repository_path)
  650. manifest, key = Manifest.load(repository)
  651. archive = Archive(repository, key, manifest, name)
  652. return archive, repository
  653. def test_check_usage(self):
  654. output = self.cmd('check', self.repository_location, exit_code=0)
  655. self.assert_in('Starting repository check', output)
  656. self.assert_in('Starting archive consistency check', output)
  657. output = self.cmd('check', '--repository-only', self.repository_location, exit_code=0)
  658. self.assert_in('Starting repository check', output)
  659. self.assert_not_in('Starting archive consistency check', output)
  660. output = self.cmd('check', '--archives-only', self.repository_location, exit_code=0)
  661. self.assert_not_in('Starting repository check', output)
  662. self.assert_in('Starting archive consistency check', output)
  663. def test_missing_file_chunk(self):
  664. archive, repository = self.open_archive('archive1')
  665. for item in archive.iter_items():
  666. if item[b'path'].endswith('testsuite/archiver.py'):
  667. repository.delete(item[b'chunks'][-1][0])
  668. break
  669. repository.commit()
  670. self.cmd('check', self.repository_location, exit_code=1)
  671. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  672. self.cmd('check', self.repository_location, exit_code=0)
  673. def test_missing_archive_item_chunk(self):
  674. archive, repository = self.open_archive('archive1')
  675. repository.delete(archive.metadata[b'items'][-5])
  676. repository.commit()
  677. self.cmd('check', self.repository_location, exit_code=1)
  678. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  679. self.cmd('check', self.repository_location, exit_code=0)
  680. def test_missing_archive_metadata(self):
  681. archive, repository = self.open_archive('archive1')
  682. repository.delete(archive.id)
  683. repository.commit()
  684. self.cmd('check', self.repository_location, exit_code=1)
  685. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  686. self.cmd('check', self.repository_location, exit_code=0)
  687. def test_missing_manifest(self):
  688. archive, repository = self.open_archive('archive1')
  689. repository.delete(Manifest.MANIFEST_ID)
  690. repository.commit()
  691. self.cmd('check', self.repository_location, exit_code=1)
  692. output = self.cmd('check', '--repair', self.repository_location, exit_code=0)
  693. self.assert_in('archive1', output)
  694. self.assert_in('archive2', output)
  695. self.cmd('check', self.repository_location, exit_code=0)
  696. def test_extra_chunks(self):
  697. self.cmd('check', self.repository_location, exit_code=0)
  698. repository = Repository(self.repository_location)
  699. repository.put(b'01234567890123456789012345678901', b'xxxx')
  700. repository.commit()
  701. repository.close()
  702. self.cmd('check', self.repository_location, exit_code=1)
  703. self.cmd('check', self.repository_location, exit_code=1)
  704. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  705. self.cmd('check', self.repository_location, exit_code=0)
  706. self.cmd('extract', '--dry-run', self.repository_location + '::archive1', exit_code=0)
  707. class RemoteArchiverTestCase(ArchiverTestCase):
  708. prefix = '__testsuite__:'
  709. def test_remote_repo_restrict_to_path(self):
  710. self.cmd('init', self.repository_location)
  711. path_prefix = os.path.dirname(self.repository_path)
  712. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo']):
  713. self.assert_raises(PathNotAllowed, lambda: self.cmd('init', self.repository_location + '_1'))
  714. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', path_prefix]):
  715. self.cmd('init', self.repository_location + '_2')
  716. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]):
  717. self.cmd('init', self.repository_location + '_3')
  718. # skip fuse tests here, they deadlock since this change in exec_cmd:
  719. # -output = subprocess.check_output(borg + args, stderr=None)
  720. # +output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT)
  721. # this was introduced because some tests expect stderr contents to show up
  722. # in "output" also. Also, the non-forking exec_cmd catches both, too.
  723. @unittest.skip('deadlock issues')
  724. def test_fuse_mount_repository(self):
  725. pass
  726. @unittest.skip('deadlock issues')
  727. def test_fuse_mount_archive(self):
  728. pass