archiver.py 58 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220
  1. from binascii import hexlify
  2. from configparser import ConfigParser
  3. import errno
  4. import os
  5. from io import StringIO
  6. import random
  7. import stat
  8. import subprocess
  9. import sys
  10. import shutil
  11. import tempfile
  12. import time
  13. import unittest
  14. from unittest.mock import patch
  15. from hashlib import sha256
  16. import pytest
  17. from .. import xattr
  18. from ..archive import Archive, ChunkBuffer, CHUNK_MAX_EXP
  19. from ..archiver import Archiver
  20. from ..cache import Cache
  21. from ..crypto import bytes_to_long, num_aes_blocks
  22. from ..helpers import Manifest, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR
  23. from ..remote import RemoteRepository, PathNotAllowed
  24. from ..repository import Repository
  25. from . import BaseTestCase, changedir, environment_variable
  26. try:
  27. import llfuse
  28. has_llfuse = True or llfuse # avoids "unused import"
  29. except ImportError:
  30. has_llfuse = False
  31. has_lchflags = hasattr(os, 'lchflags')
  32. src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
  33. def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw):
  34. if fork:
  35. try:
  36. if exe is None:
  37. borg = (sys.executable, '-m', 'borg.archiver')
  38. elif isinstance(exe, str):
  39. borg = (exe, )
  40. elif not isinstance(exe, tuple):
  41. raise ValueError('exe must be None, a tuple or a str')
  42. output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT)
  43. ret = 0
  44. except subprocess.CalledProcessError as e:
  45. output = e.output
  46. ret = e.returncode
  47. return ret, os.fsdecode(output)
  48. else:
  49. stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr
  50. try:
  51. sys.stdin = StringIO()
  52. sys.stdout = sys.stderr = output = StringIO()
  53. if archiver is None:
  54. archiver = Archiver()
  55. archiver.exit_code = EXIT_SUCCESS
  56. args = archiver.parse_args(list(args))
  57. ret = archiver.run(args)
  58. return ret, output.getvalue()
  59. finally:
  60. sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr
  61. # check if the binary "borg.exe" is available
  62. try:
  63. exec_cmd('help', exe='borg.exe', fork=True)
  64. BORG_EXES = ['python', 'binary', ]
  65. except FileNotFoundError:
  66. BORG_EXES = ['python', ]
  67. @pytest.fixture(params=BORG_EXES)
  68. def cmd(request):
  69. if request.param == 'python':
  70. exe = None
  71. elif request.param == 'binary':
  72. exe = 'borg.exe'
  73. else:
  74. raise ValueError("param must be 'python' or 'binary'")
  75. def exec_fn(*args, **kw):
  76. return exec_cmd(*args, exe=exe, fork=True, **kw)
  77. return exec_fn
  78. def test_return_codes(cmd, tmpdir):
  79. repo = tmpdir.mkdir('repo')
  80. input = tmpdir.mkdir('input')
  81. output = tmpdir.mkdir('output')
  82. input.join('test_file').write('content')
  83. rc, out = cmd('init', '--encryption=none', '%s' % str(repo))
  84. assert rc == EXIT_SUCCESS
  85. rc, out = cmd('create', '%s::archive' % repo, str(input))
  86. assert rc == EXIT_SUCCESS
  87. with changedir(str(output)):
  88. rc, out = cmd('extract', '%s::archive' % repo)
  89. assert rc == EXIT_SUCCESS
  90. rc, out = cmd('extract', '%s::archive' % repo, 'does/not/match')
  91. assert rc == EXIT_WARNING # pattern did not match
  92. rc, out = cmd('create', '%s::archive' % repo, str(input))
  93. assert rc == EXIT_ERROR # duplicate archive name
  94. """
  95. test_disk_full is very slow and not recommended to be included in daily testing.
  96. for this test, an empty, writable 16MB filesystem mounted on DF_MOUNT is required.
  97. for speed and other reasons, it is recommended that the underlying block device is
  98. in RAM, not a magnetic or flash disk.
  99. assuming /tmp is a tmpfs (in memory filesystem), one can use this:
  100. dd if=/dev/zero of=/tmp/borg-disk bs=16M count=1
  101. mkfs.ext4 /tmp/borg-disk
  102. mkdir /tmp/borg-mount
  103. sudo mount /tmp/borg-disk /tmp/borg-mount
  104. if the directory does not exist, the test will be skipped.
  105. """
  106. DF_MOUNT = '/tmp/borg-mount'
  107. @pytest.mark.skipif(not os.path.exists(DF_MOUNT), reason="needs a 16MB fs mounted on %s" % DF_MOUNT)
  108. def test_disk_full(cmd):
  109. def make_files(dir, count, size, rnd=True):
  110. shutil.rmtree(dir, ignore_errors=True)
  111. os.mkdir(dir)
  112. if rnd:
  113. count = random.randint(1, count)
  114. if size > 1:
  115. size = random.randint(1, size)
  116. for i in range(count):
  117. fn = os.path.join(dir, "file%03d" % i)
  118. with open(fn, 'wb') as f:
  119. data = os.urandom(size)
  120. f.write(data)
  121. with environment_variable(BORG_CHECK_I_KNOW_WHAT_I_AM_DOING='YES'):
  122. mount = DF_MOUNT
  123. assert os.path.exists(mount)
  124. repo = os.path.join(mount, 'repo')
  125. input = os.path.join(mount, 'input')
  126. reserve = os.path.join(mount, 'reserve')
  127. for j in range(100):
  128. shutil.rmtree(repo, ignore_errors=True)
  129. shutil.rmtree(input, ignore_errors=True)
  130. # keep some space and some inodes in reserve that we can free up later:
  131. make_files(reserve, 80, 100000, rnd=False)
  132. rc, out = cmd('init', repo)
  133. if rc != EXIT_SUCCESS:
  134. print('init', rc, out)
  135. assert rc == EXIT_SUCCESS
  136. try:
  137. success, i = True, 0
  138. while success:
  139. i += 1
  140. try:
  141. make_files(input, 20, 200000)
  142. except OSError as err:
  143. if err.errno == errno.ENOSPC:
  144. # already out of space
  145. break
  146. raise
  147. try:
  148. rc, out = cmd('create', '%s::test%03d' % (repo, i), input)
  149. success = rc == EXIT_SUCCESS
  150. if not success:
  151. print('create', rc, out)
  152. finally:
  153. # make sure repo is not locked
  154. shutil.rmtree(os.path.join(repo, 'lock.exclusive'), ignore_errors=True)
  155. os.remove(os.path.join(repo, 'lock.roster'))
  156. finally:
  157. # now some error happened, likely we are out of disk space.
  158. # free some space so we can expect borg to be able to work normally:
  159. shutil.rmtree(reserve, ignore_errors=True)
  160. rc, out = cmd('list', repo)
  161. if rc != EXIT_SUCCESS:
  162. print('list', rc, out)
  163. rc, out = cmd('check', '--repair', repo)
  164. if rc != EXIT_SUCCESS:
  165. print('check', rc, out)
  166. assert rc == EXIT_SUCCESS
  167. class ArchiverTestCaseBase(BaseTestCase):
  168. EXE = None # python source based
  169. FORK_DEFAULT = False
  170. prefix = ''
  171. def setUp(self):
  172. os.environ['BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
  173. os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
  174. os.environ['BORG_PASSPHRASE'] = 'waytooeasyonlyfortests'
  175. self.archiver = not self.FORK_DEFAULT and Archiver() or None
  176. self.tmpdir = tempfile.mkdtemp()
  177. self.repository_path = os.path.join(self.tmpdir, 'repository')
  178. self.repository_location = self.prefix + self.repository_path
  179. self.input_path = os.path.join(self.tmpdir, 'input')
  180. self.output_path = os.path.join(self.tmpdir, 'output')
  181. self.keys_path = os.path.join(self.tmpdir, 'keys')
  182. self.cache_path = os.path.join(self.tmpdir, 'cache')
  183. self.exclude_file_path = os.path.join(self.tmpdir, 'excludes')
  184. os.environ['BORG_KEYS_DIR'] = self.keys_path
  185. os.environ['BORG_CACHE_DIR'] = self.cache_path
  186. os.mkdir(self.input_path)
  187. os.mkdir(self.output_path)
  188. os.mkdir(self.keys_path)
  189. os.mkdir(self.cache_path)
  190. with open(self.exclude_file_path, 'wb') as fd:
  191. fd.write(b'input/file2\n# A comment line, then a blank line\n\n')
  192. self._old_wd = os.getcwd()
  193. os.chdir(self.tmpdir)
  194. def tearDown(self):
  195. os.chdir(self._old_wd)
  196. shutil.rmtree(self.tmpdir)
  197. def cmd(self, *args, **kw):
  198. exit_code = kw.pop('exit_code', 0)
  199. fork = kw.pop('fork', None)
  200. if fork is None:
  201. fork = self.FORK_DEFAULT
  202. ret, output = exec_cmd(*args, fork=fork, exe=self.EXE, archiver=self.archiver, **kw)
  203. if ret != exit_code:
  204. print(output)
  205. self.assert_equal(ret, exit_code)
  206. return output
  207. def create_src_archive(self, name):
  208. self.cmd('create', self.repository_location + '::' + name, src_dir)
  209. class ArchiverTestCase(ArchiverTestCaseBase):
  210. def create_regular_file(self, name, size=0, contents=None):
  211. filename = os.path.join(self.input_path, name)
  212. if not os.path.exists(os.path.dirname(filename)):
  213. os.makedirs(os.path.dirname(filename))
  214. with open(filename, 'wb') as fd:
  215. if contents is None:
  216. contents = b'X' * size
  217. fd.write(contents)
  218. def create_test_files(self):
  219. """Create a minimal test case including all supported file types
  220. """
  221. # File
  222. self.create_regular_file('empty', size=0)
  223. # next code line raises OverflowError on 32bit cpu (raspberry pi 2):
  224. # 2600-01-01 > 2**64 ns
  225. # os.utime('input/empty', (19880895600, 19880895600))
  226. # thus, we better test with something not that far in future:
  227. # 2038-01-19 (1970 + 2^31 - 1 seconds) is the 32bit "deadline":
  228. os.utime('input/empty', (2**31 - 1, 2**31 - 1))
  229. self.create_regular_file('file1', size=1024 * 80)
  230. self.create_regular_file('flagfile', size=1024)
  231. # Directory
  232. self.create_regular_file('dir2/file2', size=1024 * 80)
  233. # File mode
  234. os.chmod('input/file1', 0o4755)
  235. # Hard link
  236. os.link(os.path.join(self.input_path, 'file1'),
  237. os.path.join(self.input_path, 'hardlink'))
  238. # Symlink
  239. os.symlink('somewhere', os.path.join(self.input_path, 'link1'))
  240. if xattr.is_enabled(self.input_path):
  241. xattr.setxattr(os.path.join(self.input_path, 'file1'), 'user.foo', b'bar')
  242. # XXX this always fails for me
  243. # ubuntu 14.04, on a TMP dir filesystem with user_xattr, using fakeroot
  244. # same for newer ubuntu and centos.
  245. # if this is supported just on specific platform, platform should be checked first,
  246. # so that the test setup for all tests using it does not fail here always for others.
  247. # xattr.setxattr(os.path.join(self.input_path, 'link1'), 'user.foo_symlink', b'bar_symlink', follow_symlinks=False)
  248. # FIFO node
  249. os.mkfifo(os.path.join(self.input_path, 'fifo1'))
  250. if has_lchflags:
  251. os.lchflags(os.path.join(self.input_path, 'flagfile'), stat.UF_NODUMP)
  252. try:
  253. # Block device
  254. os.mknod('input/bdev', 0o600 | stat.S_IFBLK, os.makedev(10, 20))
  255. # Char device
  256. os.mknod('input/cdev', 0o600 | stat.S_IFCHR, os.makedev(30, 40))
  257. # File mode
  258. os.chmod('input/dir2', 0o555) # if we take away write perms, we need root to remove contents
  259. # File owner
  260. os.chown('input/file1', 100, 200) # raises OSError invalid argument on cygwin
  261. have_root = True # we have (fake)root
  262. except PermissionError:
  263. have_root = False
  264. except OSError as e:
  265. if e.errno != errno.EINVAL:
  266. raise
  267. have_root = False
  268. return have_root
  269. def test_basic_functionality(self):
  270. have_root = self.create_test_files()
  271. self.cmd('init', self.repository_location)
  272. self.cmd('create', self.repository_location + '::test', 'input')
  273. self.cmd('create', '--stats', self.repository_location + '::test.2', 'input')
  274. with changedir('output'):
  275. self.cmd('extract', self.repository_location + '::test')
  276. list_output = self.cmd('list', '--short', self.repository_location)
  277. self.assert_in('test', list_output)
  278. self.assert_in('test.2', list_output)
  279. expected = [
  280. 'input',
  281. 'input/bdev',
  282. 'input/cdev',
  283. 'input/dir2',
  284. 'input/dir2/file2',
  285. 'input/empty',
  286. 'input/fifo1',
  287. 'input/file1',
  288. 'input/flagfile',
  289. 'input/hardlink',
  290. 'input/link1',
  291. ]
  292. if not have_root:
  293. # we could not create these device files without (fake)root
  294. expected.remove('input/bdev')
  295. expected.remove('input/cdev')
  296. if has_lchflags:
  297. # remove the file we did not backup, so input and output become equal
  298. expected.remove('input/flagfile') # this file is UF_NODUMP
  299. os.remove(os.path.join('input', 'flagfile'))
  300. list_output = self.cmd('list', '--short', self.repository_location + '::test')
  301. for name in expected:
  302. self.assert_in(name, list_output)
  303. self.assert_dirs_equal('input', 'output/input')
  304. info_output = self.cmd('info', self.repository_location + '::test')
  305. item_count = 3 if has_lchflags else 4 # one file is UF_NODUMP
  306. self.assert_in('Number of files: %d' % item_count, info_output)
  307. shutil.rmtree(self.cache_path)
  308. with environment_variable(BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='yes'):
  309. info_output2 = self.cmd('info', self.repository_location + '::test')
  310. def filter(output):
  311. # filter for interesting "info" output, ignore cache rebuilding related stuff
  312. prefixes = ['Name:', 'Fingerprint:', 'Number of files:', 'This archive:',
  313. 'All archives:', 'Chunk index:', ]
  314. result = []
  315. for line in output.splitlines():
  316. for prefix in prefixes:
  317. if line.startswith(prefix):
  318. result.append(line)
  319. return '\n'.join(result)
  320. # the interesting parts of info_output2 and info_output should be same
  321. self.assert_equal(filter(info_output), filter(info_output2))
  322. def test_atime(self):
  323. self.create_test_files()
  324. atime, mtime = 123456780, 234567890
  325. os.utime('input/file1', (atime, mtime))
  326. self.cmd('init', self.repository_location)
  327. self.cmd('create', self.repository_location + '::test', 'input')
  328. with changedir('output'):
  329. self.cmd('extract', self.repository_location + '::test')
  330. sti = os.stat('input/file1')
  331. sto = os.stat('output/input/file1')
  332. assert sti.st_mtime_ns == sto.st_mtime_ns == mtime * 1e9
  333. if hasattr(os, 'O_NOATIME'):
  334. assert sti.st_atime_ns == sto.st_atime_ns == atime * 1e9
  335. else:
  336. # it touched the input file's atime while backing it up
  337. assert sto.st_atime_ns == atime * 1e9
  338. def _extract_repository_id(self, path):
  339. with Repository(self.repository_path) as repository:
  340. return repository.id
  341. def _set_repository_id(self, path, id):
  342. config = ConfigParser(interpolation=None)
  343. config.read(os.path.join(path, 'config'))
  344. config.set('repository', 'id', hexlify(id).decode('ascii'))
  345. with open(os.path.join(path, 'config'), 'w') as fd:
  346. config.write(fd)
  347. with Repository(self.repository_path) as repository:
  348. return repository.id
  349. def test_sparse_file(self):
  350. # no sparse file support on Mac OS X
  351. sparse_support = sys.platform != 'darwin'
  352. filename = os.path.join(self.input_path, 'sparse')
  353. content = b'foobar'
  354. hole_size = 5 * (1 << CHUNK_MAX_EXP) # 5 full chunker buffers
  355. with open(filename, 'wb') as fd:
  356. # create a file that has a hole at the beginning and end (if the
  357. # OS and filesystem supports sparse files)
  358. fd.seek(hole_size, 1)
  359. fd.write(content)
  360. fd.seek(hole_size, 1)
  361. pos = fd.tell()
  362. fd.truncate(pos)
  363. total_len = hole_size + len(content) + hole_size
  364. st = os.stat(filename)
  365. self.assert_equal(st.st_size, total_len)
  366. if sparse_support and hasattr(st, 'st_blocks'):
  367. self.assert_true(st.st_blocks * 512 < total_len) # is input sparse?
  368. self.cmd('init', self.repository_location)
  369. self.cmd('create', self.repository_location + '::test', 'input')
  370. with changedir('output'):
  371. self.cmd('extract', '--sparse', self.repository_location + '::test')
  372. self.assert_dirs_equal('input', 'output/input')
  373. filename = os.path.join(self.output_path, 'input', 'sparse')
  374. with open(filename, 'rb') as fd:
  375. # check if file contents are as expected
  376. self.assert_equal(fd.read(hole_size), b'\0' * hole_size)
  377. self.assert_equal(fd.read(len(content)), content)
  378. self.assert_equal(fd.read(hole_size), b'\0' * hole_size)
  379. st = os.stat(filename)
  380. self.assert_equal(st.st_size, total_len)
  381. if sparse_support:
  382. if hasattr(st, 'st_blocks'):
  383. # do only check if it is less, do NOT check if it is much less
  384. # as that causes troubles on xfs, zfs, ntfs:
  385. self.assert_true(st.st_blocks * 512 < total_len)
  386. if hasattr(os, 'SEEK_HOLE') and hasattr(os, 'SEEK_DATA'):
  387. with open(filename, 'rb') as fd:
  388. # only check if the first hole is as expected, because the 2nd hole check
  389. # is problematic on xfs due to its "dynamic speculative EOF preallocation
  390. try:
  391. self.assert_equal(fd.seek(0, os.SEEK_HOLE), 0)
  392. self.assert_equal(fd.seek(0, os.SEEK_DATA), hole_size)
  393. except OSError:
  394. # does not really support SEEK_HOLE/SEEK_DATA
  395. pass
  396. def test_unusual_filenames(self):
  397. filenames = ['normal', 'with some blanks', '(with_parens)', ]
  398. for filename in filenames:
  399. filename = os.path.join(self.input_path, filename)
  400. with open(filename, 'wb'):
  401. pass
  402. self.cmd('init', self.repository_location)
  403. self.cmd('create', self.repository_location + '::test', 'input')
  404. for filename in filenames:
  405. with changedir('output'):
  406. self.cmd('extract', self.repository_location + '::test', os.path.join('input', filename))
  407. assert os.path.exists(os.path.join('output', 'input', filename))
  408. def test_repository_swap_detection(self):
  409. self.create_test_files()
  410. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  411. self.cmd('init', '--encryption=repokey', self.repository_location)
  412. repository_id = self._extract_repository_id(self.repository_path)
  413. self.cmd('create', self.repository_location + '::test', 'input')
  414. shutil.rmtree(self.repository_path)
  415. self.cmd('init', '--encryption=none', self.repository_location)
  416. self._set_repository_id(self.repository_path, repository_id)
  417. self.assert_equal(repository_id, self._extract_repository_id(self.repository_path))
  418. if self.FORK_DEFAULT:
  419. self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR)
  420. else:
  421. self.assert_raises(Cache.EncryptionMethodMismatch, lambda: self.cmd('create', self.repository_location + '::test.2', 'input'))
  422. def test_repository_swap_detection2(self):
  423. self.create_test_files()
  424. self.cmd('init', '--encryption=none', self.repository_location + '_unencrypted')
  425. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  426. self.cmd('init', '--encryption=repokey', self.repository_location + '_encrypted')
  427. self.cmd('create', self.repository_location + '_encrypted::test', 'input')
  428. shutil.rmtree(self.repository_path + '_encrypted')
  429. os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted')
  430. if self.FORK_DEFAULT:
  431. self.cmd('create', self.repository_location + '_encrypted::test.2', 'input', exit_code=EXIT_ERROR)
  432. else:
  433. self.assert_raises(Cache.RepositoryAccessAborted, lambda: self.cmd('create', self.repository_location + '_encrypted::test.2', 'input'))
  434. def test_strip_components(self):
  435. self.cmd('init', self.repository_location)
  436. self.create_regular_file('dir/file')
  437. self.cmd('create', self.repository_location + '::test', 'input')
  438. with changedir('output'):
  439. self.cmd('extract', self.repository_location + '::test', '--strip-components', '3')
  440. self.assert_true(not os.path.exists('file'))
  441. with self.assert_creates_file('file'):
  442. self.cmd('extract', self.repository_location + '::test', '--strip-components', '2')
  443. with self.assert_creates_file('dir/file'):
  444. self.cmd('extract', self.repository_location + '::test', '--strip-components', '1')
  445. with self.assert_creates_file('input/dir/file'):
  446. self.cmd('extract', self.repository_location + '::test', '--strip-components', '0')
  447. def test_extract_include_exclude(self):
  448. self.cmd('init', self.repository_location)
  449. self.create_regular_file('file1', size=1024 * 80)
  450. self.create_regular_file('file2', size=1024 * 80)
  451. self.create_regular_file('file3', size=1024 * 80)
  452. self.create_regular_file('file4', size=1024 * 80)
  453. self.cmd('create', '--exclude=input/file4', self.repository_location + '::test', 'input')
  454. with changedir('output'):
  455. self.cmd('extract', self.repository_location + '::test', 'input/file1', )
  456. self.assert_equal(sorted(os.listdir('output/input')), ['file1'])
  457. with changedir('output'):
  458. self.cmd('extract', '--exclude=input/file2', self.repository_location + '::test')
  459. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  460. with changedir('output'):
  461. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  462. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  463. def test_extract_include_exclude_regex(self):
  464. self.cmd('init', self.repository_location)
  465. self.create_regular_file('file1', size=1024 * 80)
  466. self.create_regular_file('file2', size=1024 * 80)
  467. self.create_regular_file('file3', size=1024 * 80)
  468. self.create_regular_file('file4', size=1024 * 80)
  469. self.create_regular_file('file333', size=1024 * 80)
  470. # Create with regular expression exclusion for file4
  471. self.cmd('create', '--exclude=re:input/file4$', self.repository_location + '::test', 'input')
  472. with changedir('output'):
  473. self.cmd('extract', self.repository_location + '::test')
  474. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333'])
  475. shutil.rmtree('output/input')
  476. # Extract with regular expression exclusion
  477. with changedir('output'):
  478. self.cmd('extract', '--exclude=re:file3+', self.repository_location + '::test')
  479. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2'])
  480. shutil.rmtree('output/input')
  481. # Combine --exclude with fnmatch and regular expression
  482. with changedir('output'):
  483. self.cmd('extract', '--exclude=input/file2', '--exclude=re:file[01]', self.repository_location + '::test')
  484. self.assert_equal(sorted(os.listdir('output/input')), ['file3', 'file333'])
  485. shutil.rmtree('output/input')
  486. # Combine --exclude-from and regular expression exclusion
  487. with changedir('output'):
  488. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, '--exclude=re:file1',
  489. '--exclude=re:file(\\d)\\1\\1$', self.repository_location + '::test')
  490. self.assert_equal(sorted(os.listdir('output/input')), ['file3'])
  491. def test_extract_include_exclude_regex_from_file(self):
  492. self.cmd('init', self.repository_location)
  493. self.create_regular_file('file1', size=1024 * 80)
  494. self.create_regular_file('file2', size=1024 * 80)
  495. self.create_regular_file('file3', size=1024 * 80)
  496. self.create_regular_file('file4', size=1024 * 80)
  497. self.create_regular_file('file333', size=1024 * 80)
  498. self.create_regular_file('aa:something', size=1024 * 80)
  499. # Create while excluding using mixed pattern styles
  500. with open(self.exclude_file_path, 'wb') as fd:
  501. fd.write(b're:input/file4$\n')
  502. fd.write(b'fm:*aa:*thing\n')
  503. self.cmd('create', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test', 'input')
  504. with changedir('output'):
  505. self.cmd('extract', self.repository_location + '::test')
  506. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333'])
  507. shutil.rmtree('output/input')
  508. # Exclude using regular expression
  509. with open(self.exclude_file_path, 'wb') as fd:
  510. fd.write(b're:file3+\n')
  511. with changedir('output'):
  512. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  513. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2'])
  514. shutil.rmtree('output/input')
  515. # Mixed exclude pattern styles
  516. with open(self.exclude_file_path, 'wb') as fd:
  517. fd.write(b're:file(\\d)\\1\\1$\n')
  518. fd.write(b'fm:nothingwillmatchthis\n')
  519. fd.write(b'*/file1\n')
  520. fd.write(b're:file2$\n')
  521. with changedir('output'):
  522. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  523. self.assert_equal(sorted(os.listdir('output/input')), ['file3'])
  524. def test_extract_with_pattern(self):
  525. self.cmd("init", self.repository_location)
  526. self.create_regular_file("file1", size=1024 * 80)
  527. self.create_regular_file("file2", size=1024 * 80)
  528. self.create_regular_file("file3", size=1024 * 80)
  529. self.create_regular_file("file4", size=1024 * 80)
  530. self.create_regular_file("file333", size=1024 * 80)
  531. self.cmd("create", self.repository_location + "::test", "input")
  532. # Extract everything with regular expression
  533. with changedir("output"):
  534. self.cmd("extract", self.repository_location + "::test", "re:.*")
  535. self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file3", "file333", "file4"])
  536. shutil.rmtree("output/input")
  537. # Extract with pattern while also excluding files
  538. with changedir("output"):
  539. self.cmd("extract", "--exclude=re:file[34]$", self.repository_location + "::test", r"re:file\d$")
  540. self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2"])
  541. shutil.rmtree("output/input")
  542. # Combine --exclude with pattern for extraction
  543. with changedir("output"):
  544. self.cmd("extract", "--exclude=input/file1", self.repository_location + "::test", "re:file[12]$")
  545. self.assert_equal(sorted(os.listdir("output/input")), ["file2"])
  546. shutil.rmtree("output/input")
  547. # Multiple pattern
  548. with changedir("output"):
  549. self.cmd("extract", self.repository_location + "::test", "fm:input/file1", "fm:*file33*", "input/file2")
  550. self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file333"])
  551. def test_exclude_caches(self):
  552. self.cmd('init', self.repository_location)
  553. self.create_regular_file('file1', size=1024 * 80)
  554. self.create_regular_file('cache1/CACHEDIR.TAG', contents=b'Signature: 8a477f597d28d172789f06886806bc55 extra stuff')
  555. self.create_regular_file('cache2/CACHEDIR.TAG', contents=b'invalid signature')
  556. self.cmd('create', '--exclude-caches', self.repository_location + '::test', 'input')
  557. with changedir('output'):
  558. self.cmd('extract', self.repository_location + '::test')
  559. self.assert_equal(sorted(os.listdir('output/input')), ['cache2', 'file1'])
  560. self.assert_equal(sorted(os.listdir('output/input/cache2')), ['CACHEDIR.TAG'])
  561. def test_exclude_tagged(self):
  562. self.cmd('init', self.repository_location)
  563. self.create_regular_file('file1', size=1024 * 80)
  564. self.create_regular_file('tagged1/.NOBACKUP')
  565. self.create_regular_file('tagged2/00-NOBACKUP')
  566. self.create_regular_file('tagged3/.NOBACKUP/file2')
  567. self.cmd('create', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP', self.repository_location + '::test', 'input')
  568. with changedir('output'):
  569. self.cmd('extract', self.repository_location + '::test')
  570. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'tagged3'])
  571. def test_exclude_keep_tagged(self):
  572. self.cmd('init', self.repository_location)
  573. self.create_regular_file('file0', size=1024)
  574. self.create_regular_file('tagged1/.NOBACKUP1')
  575. self.create_regular_file('tagged1/file1', size=1024)
  576. self.create_regular_file('tagged2/.NOBACKUP2')
  577. self.create_regular_file('tagged2/file2', size=1024)
  578. self.create_regular_file('tagged3/CACHEDIR.TAG', contents=b'Signature: 8a477f597d28d172789f06886806bc55 extra stuff')
  579. self.create_regular_file('tagged3/file3', size=1024)
  580. self.create_regular_file('taggedall/.NOBACKUP1')
  581. self.create_regular_file('taggedall/.NOBACKUP2')
  582. self.create_regular_file('taggedall/CACHEDIR.TAG', contents=b'Signature: 8a477f597d28d172789f06886806bc55 extra stuff')
  583. self.create_regular_file('taggedall/file4', size=1024)
  584. self.cmd('create', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2',
  585. '--exclude-caches', '--keep-tag-files', self.repository_location + '::test', 'input')
  586. with changedir('output'):
  587. self.cmd('extract', self.repository_location + '::test')
  588. self.assert_equal(sorted(os.listdir('output/input')), ['file0', 'tagged1', 'tagged2', 'tagged3', 'taggedall'])
  589. self.assert_equal(os.listdir('output/input/tagged1'), ['.NOBACKUP1'])
  590. self.assert_equal(os.listdir('output/input/tagged2'), ['.NOBACKUP2'])
  591. self.assert_equal(os.listdir('output/input/tagged3'), ['CACHEDIR.TAG'])
  592. self.assert_equal(sorted(os.listdir('output/input/taggedall')),
  593. ['.NOBACKUP1', '.NOBACKUP2', 'CACHEDIR.TAG', ])
  594. @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='Linux capabilities test, requires fakeroot >= 1.20.2')
  595. def test_extract_capabilities(self):
  596. fchown = os.fchown
  597. # We need to manually patch chown to get the behaviour Linux has, since fakeroot does not
  598. # accurately model the interaction of chown(2) and Linux capabilities, i.e. it does not remove them.
  599. def patched_fchown(fd, uid, gid):
  600. xattr.setxattr(fd, 'security.capability', None, follow_symlinks=False)
  601. fchown(fd, uid, gid)
  602. # The capability descriptor used here is valid and taken from a /usr/bin/ping
  603. capabilities = b'\x01\x00\x00\x02\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
  604. self.create_regular_file('file')
  605. xattr.setxattr('input/file', 'security.capability', capabilities)
  606. self.cmd('init', self.repository_location)
  607. self.cmd('create', self.repository_location + '::test', 'input')
  608. with changedir('output'):
  609. with patch.object(os, 'fchown', patched_fchown):
  610. self.cmd('extract', self.repository_location + '::test')
  611. assert xattr.getxattr('input/file', 'security.capability') == capabilities
  612. def test_path_normalization(self):
  613. self.cmd('init', self.repository_location)
  614. self.create_regular_file('dir1/dir2/file', size=1024 * 80)
  615. with changedir('input/dir1/dir2'):
  616. self.cmd('create', self.repository_location + '::test', '../../../input/dir1/../dir1/dir2/..')
  617. output = self.cmd('list', self.repository_location + '::test')
  618. self.assert_not_in('..', output)
  619. self.assert_in(' input/dir1/dir2/file', output)
  620. def test_exclude_normalization(self):
  621. self.cmd('init', self.repository_location)
  622. self.create_regular_file('file1', size=1024 * 80)
  623. self.create_regular_file('file2', size=1024 * 80)
  624. with changedir('input'):
  625. self.cmd('create', '--exclude=file1', self.repository_location + '::test1', '.')
  626. with changedir('output'):
  627. self.cmd('extract', self.repository_location + '::test1')
  628. self.assert_equal(sorted(os.listdir('output')), ['file2'])
  629. with changedir('input'):
  630. self.cmd('create', '--exclude=./file1', self.repository_location + '::test2', '.')
  631. with changedir('output'):
  632. self.cmd('extract', self.repository_location + '::test2')
  633. self.assert_equal(sorted(os.listdir('output')), ['file2'])
  634. self.cmd('create', '--exclude=input/./file1', self.repository_location + '::test3', 'input')
  635. with changedir('output'):
  636. self.cmd('extract', self.repository_location + '::test3')
  637. self.assert_equal(sorted(os.listdir('output/input')), ['file2'])
  638. def test_repeated_files(self):
  639. self.create_regular_file('file1', size=1024 * 80)
  640. self.cmd('init', self.repository_location)
  641. self.cmd('create', self.repository_location + '::test', 'input', 'input')
  642. def test_overwrite(self):
  643. self.create_regular_file('file1', size=1024 * 80)
  644. self.create_regular_file('dir2/file2', size=1024 * 80)
  645. self.cmd('init', self.repository_location)
  646. self.cmd('create', self.repository_location + '::test', 'input')
  647. # Overwriting regular files and directories should be supported
  648. os.mkdir('output/input')
  649. os.mkdir('output/input/file1')
  650. os.mkdir('output/input/dir2')
  651. with changedir('output'):
  652. self.cmd('extract', self.repository_location + '::test')
  653. self.assert_dirs_equal('input', 'output/input')
  654. # But non-empty dirs should fail
  655. os.unlink('output/input/file1')
  656. os.mkdir('output/input/file1')
  657. os.mkdir('output/input/file1/dir')
  658. with changedir('output'):
  659. self.cmd('extract', self.repository_location + '::test', exit_code=1)
  660. def test_rename(self):
  661. self.create_regular_file('file1', size=1024 * 80)
  662. self.create_regular_file('dir2/file2', size=1024 * 80)
  663. self.cmd('init', self.repository_location)
  664. self.cmd('create', self.repository_location + '::test', 'input')
  665. self.cmd('create', self.repository_location + '::test.2', 'input')
  666. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  667. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  668. self.cmd('rename', self.repository_location + '::test', 'test.3')
  669. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  670. self.cmd('rename', self.repository_location + '::test.2', 'test.4')
  671. self.cmd('extract', '--dry-run', self.repository_location + '::test.3')
  672. self.cmd('extract', '--dry-run', self.repository_location + '::test.4')
  673. # Make sure both archives have been renamed
  674. with Repository(self.repository_path) as repository:
  675. manifest, key = Manifest.load(repository)
  676. self.assert_equal(len(manifest.archives), 2)
  677. self.assert_in('test.3', manifest.archives)
  678. self.assert_in('test.4', manifest.archives)
  679. def test_delete(self):
  680. self.create_regular_file('file1', size=1024 * 80)
  681. self.create_regular_file('dir2/file2', size=1024 * 80)
  682. self.cmd('init', self.repository_location)
  683. self.cmd('create', self.repository_location + '::test', 'input')
  684. self.cmd('create', self.repository_location + '::test.2', 'input')
  685. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  686. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  687. self.cmd('delete', self.repository_location + '::test')
  688. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  689. self.cmd('delete', '--stats', self.repository_location + '::test.2')
  690. # Make sure all data except the manifest has been deleted
  691. with Repository(self.repository_path) as repository:
  692. self.assert_equal(len(repository), 1)
  693. def test_delete_repo(self):
  694. self.create_regular_file('file1', size=1024 * 80)
  695. self.create_regular_file('dir2/file2', size=1024 * 80)
  696. self.cmd('init', self.repository_location)
  697. self.cmd('create', self.repository_location + '::test', 'input')
  698. self.cmd('create', self.repository_location + '::test.2', 'input')
  699. os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'no'
  700. self.cmd('delete', self.repository_location, exit_code=2)
  701. assert os.path.exists(self.repository_path)
  702. os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
  703. self.cmd('delete', self.repository_location)
  704. # Make sure the repo is gone
  705. self.assertFalse(os.path.exists(self.repository_path))
  706. def test_corrupted_repository(self):
  707. self.cmd('init', self.repository_location)
  708. self.create_src_archive('test')
  709. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  710. self.cmd('check', self.repository_location)
  711. name = sorted(os.listdir(os.path.join(self.tmpdir, 'repository', 'data', '0')), reverse=True)[0]
  712. with open(os.path.join(self.tmpdir, 'repository', 'data', '0', name), 'r+b') as fd:
  713. fd.seek(100)
  714. fd.write(b'XXXX')
  715. self.cmd('check', self.repository_location, exit_code=1)
  716. # we currently need to be able to create a lock directory inside the repo:
  717. @pytest.mark.xfail(reason="we need to be able to create the lock directory inside the repo")
  718. def test_readonly_repository(self):
  719. self.cmd('init', self.repository_location)
  720. self.create_src_archive('test')
  721. os.system('chmod -R ugo-w ' + self.repository_path)
  722. try:
  723. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  724. finally:
  725. # Restore permissions so shutil.rmtree is able to delete it
  726. os.system('chmod -R u+w ' + self.repository_path)
  727. def test_umask(self):
  728. self.create_regular_file('file1', size=1024 * 80)
  729. self.cmd('init', self.repository_location)
  730. self.cmd('create', self.repository_location + '::test', 'input')
  731. mode = os.stat(self.repository_path).st_mode
  732. self.assertEqual(stat.S_IMODE(mode), 0o700)
  733. def test_create_dry_run(self):
  734. self.cmd('init', self.repository_location)
  735. self.cmd('create', '--dry-run', self.repository_location + '::test', 'input')
  736. # Make sure no archive has been created
  737. with Repository(self.repository_path) as repository:
  738. manifest, key = Manifest.load(repository)
  739. self.assert_equal(len(manifest.archives), 0)
  740. def test_progress(self):
  741. self.create_regular_file('file1', size=1024 * 80)
  742. self.cmd('init', self.repository_location)
  743. # progress forced on
  744. output = self.cmd('create', '--progress', self.repository_location + '::test4', 'input')
  745. self.assert_in("\r", output)
  746. # progress forced off
  747. output = self.cmd('create', self.repository_location + '::test5', 'input')
  748. self.assert_not_in("\r", output)
  749. def test_file_status(self):
  750. """test that various file status show expected results
  751. clearly incomplete: only tests for the weird "unchanged" status for now"""
  752. now = time.time()
  753. self.create_regular_file('file1', size=1024 * 80)
  754. os.utime('input/file1', (now - 5, now - 5)) # 5 seconds ago
  755. self.create_regular_file('file2', size=1024 * 80)
  756. self.cmd('init', self.repository_location)
  757. output = self.cmd('create', '-v', '--list', self.repository_location + '::test', 'input')
  758. self.assert_in("A input/file1", output)
  759. self.assert_in("A input/file2", output)
  760. # should find first file as unmodified
  761. output = self.cmd('create', '-v', '--list', self.repository_location + '::test1', 'input')
  762. self.assert_in("U input/file1", output)
  763. # this is expected, although surprising, for why, see:
  764. # https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file
  765. self.assert_in("A input/file2", output)
  766. def test_create_topical(self):
  767. now = time.time()
  768. self.create_regular_file('file1', size=1024 * 80)
  769. os.utime('input/file1', (now-5, now-5))
  770. self.create_regular_file('file2', size=1024 * 80)
  771. self.cmd('init', self.repository_location)
  772. # no listing by default
  773. output = self.cmd('create', self.repository_location + '::test', 'input')
  774. self.assert_not_in('file1', output)
  775. # shouldn't be listed even if unchanged
  776. output = self.cmd('create', self.repository_location + '::test0', 'input')
  777. self.assert_not_in('file1', output)
  778. # should list the file as unchanged
  779. output = self.cmd('create', '-v', '--list', '--filter=U', self.repository_location + '::test1', 'input')
  780. self.assert_in('file1', output)
  781. # should *not* list the file as changed
  782. output = self.cmd('create', '-v', '--filter=AM', self.repository_location + '::test2', 'input')
  783. self.assert_not_in('file1', output)
  784. # change the file
  785. self.create_regular_file('file1', size=1024 * 100)
  786. # should list the file as changed
  787. output = self.cmd('create', '-v', '--list', '--filter=AM', self.repository_location + '::test3', 'input')
  788. self.assert_in('file1', output)
  789. # def test_cmdline_compatibility(self):
  790. # self.create_regular_file('file1', size=1024 * 80)
  791. # self.cmd('init', self.repository_location)
  792. # self.cmd('create', self.repository_location + '::test', 'input')
  793. # output = self.cmd('foo', self.repository_location, '--old')
  794. # self.assert_in('"--old" has been deprecated. Use "--new" instead', output)
  795. def test_prune_repository(self):
  796. self.cmd('init', self.repository_location)
  797. self.cmd('create', self.repository_location + '::test1', src_dir)
  798. self.cmd('create', self.repository_location + '::test2', src_dir)
  799. # these are not really a checkpoints, but they look like some:
  800. self.cmd('create', self.repository_location + '::test3.checkpoint', src_dir)
  801. self.cmd('create', self.repository_location + '::test3.checkpoint.1', src_dir)
  802. output = self.cmd('prune', '-v', '--list', '--dry-run', self.repository_location, '--keep-daily=2')
  803. self.assert_in('Would prune: test1', output)
  804. # must keep the latest non-checkpoint archive:
  805. self.assert_in('Keeping archive: test2', output)
  806. output = self.cmd('list', self.repository_location)
  807. self.assert_in('test1', output)
  808. self.assert_in('test2', output)
  809. self.assert_in('test3.checkpoint', output)
  810. self.assert_in('test3.checkpoint.1', output)
  811. self.cmd('prune', self.repository_location, '--keep-daily=2')
  812. output = self.cmd('list', self.repository_location)
  813. self.assert_not_in('test1', output)
  814. # the latest non-checkpoint archive must be still there:
  815. self.assert_in('test2', output)
  816. def test_prune_repository_save_space(self):
  817. self.cmd('init', self.repository_location)
  818. self.cmd('create', self.repository_location + '::test1', src_dir)
  819. self.cmd('create', self.repository_location + '::test2', src_dir)
  820. output = self.cmd('prune', '-v', '--list', '--dry-run', self.repository_location, '--keep-daily=2')
  821. self.assert_in('Keeping archive: test2', output)
  822. self.assert_in('Would prune: test1', output)
  823. output = self.cmd('list', self.repository_location)
  824. self.assert_in('test1', output)
  825. self.assert_in('test2', output)
  826. self.cmd('prune', '--save-space', self.repository_location, '--keep-daily=2')
  827. output = self.cmd('list', self.repository_location)
  828. self.assert_not_in('test1', output)
  829. self.assert_in('test2', output)
  830. def test_prune_repository_prefix(self):
  831. self.cmd('init', self.repository_location)
  832. self.cmd('create', self.repository_location + '::foo-2015-08-12-10:00', src_dir)
  833. self.cmd('create', self.repository_location + '::foo-2015-08-12-20:00', src_dir)
  834. self.cmd('create', self.repository_location + '::bar-2015-08-12-10:00', src_dir)
  835. self.cmd('create', self.repository_location + '::bar-2015-08-12-20:00', src_dir)
  836. output = self.cmd('prune', '-v', '--list', '--dry-run', self.repository_location, '--keep-daily=2', '--prefix=foo-')
  837. self.assert_in('Keeping archive: foo-2015-08-12-20:00', output)
  838. self.assert_in('Would prune: foo-2015-08-12-10:00', output)
  839. output = self.cmd('list', self.repository_location)
  840. self.assert_in('foo-2015-08-12-10:00', output)
  841. self.assert_in('foo-2015-08-12-20:00', output)
  842. self.assert_in('bar-2015-08-12-10:00', output)
  843. self.assert_in('bar-2015-08-12-20:00', output)
  844. self.cmd('prune', self.repository_location, '--keep-daily=2', '--prefix=foo-')
  845. output = self.cmd('list', self.repository_location)
  846. self.assert_not_in('foo-2015-08-12-10:00', output)
  847. self.assert_in('foo-2015-08-12-20:00', output)
  848. self.assert_in('bar-2015-08-12-10:00', output)
  849. self.assert_in('bar-2015-08-12-20:00', output)
  850. def test_list_prefix(self):
  851. self.cmd('init', self.repository_location)
  852. self.cmd('create', self.repository_location + '::test-1', src_dir)
  853. self.cmd('create', self.repository_location + '::something-else-than-test-1', src_dir)
  854. self.cmd('create', self.repository_location + '::test-2', src_dir)
  855. output = self.cmd('list', '--prefix=test-', self.repository_location)
  856. self.assert_in('test-1', output)
  857. self.assert_in('test-2', output)
  858. self.assert_not_in('something-else', output)
  859. def test_list_list_format(self):
  860. self.cmd('init', self.repository_location)
  861. test_archive = self.repository_location + '::test'
  862. self.cmd('create', test_archive, src_dir)
  863. output_1 = self.cmd('list', test_archive)
  864. output_2 = self.cmd('list', '--list-format', '{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}', test_archive)
  865. output_3 = self.cmd('list', '--list-format', '{mtime:%s} {path}{NEWLINE}', test_archive)
  866. self.assertEqual(output_1, output_2)
  867. self.assertNotEqual(output_1, output_3)
  868. def test_break_lock(self):
  869. self.cmd('init', self.repository_location)
  870. self.cmd('break-lock', self.repository_location)
  871. def test_usage(self):
  872. if self.FORK_DEFAULT:
  873. self.cmd(exit_code=0)
  874. self.cmd('-h', exit_code=0)
  875. else:
  876. self.assert_raises(SystemExit, lambda: self.cmd())
  877. self.assert_raises(SystemExit, lambda: self.cmd('-h'))
  878. def test_help(self):
  879. assert 'Borg' in self.cmd('help')
  880. assert 'patterns' in self.cmd('help', 'patterns')
  881. assert 'Initialize' in self.cmd('help', 'init')
  882. assert 'positional arguments' not in self.cmd('help', 'init', '--epilog-only')
  883. assert 'This command initializes' not in self.cmd('help', 'init', '--usage-only')
  884. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  885. def test_fuse_mount_repository(self):
  886. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  887. os.mkdir(mountpoint)
  888. self.cmd('init', self.repository_location)
  889. self.create_test_files()
  890. self.cmd('create', self.repository_location + '::archive', 'input')
  891. self.cmd('create', self.repository_location + '::archive2', 'input')
  892. try:
  893. self.cmd('mount', self.repository_location, mountpoint, fork=True)
  894. self.wait_for_mount(mountpoint)
  895. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive', 'input'))
  896. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive2', 'input'))
  897. finally:
  898. if sys.platform.startswith('linux'):
  899. os.system('fusermount -u ' + mountpoint)
  900. else:
  901. os.system('umount ' + mountpoint)
  902. os.rmdir(mountpoint)
  903. # Give the daemon some time to exit
  904. time.sleep(.2)
  905. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  906. def test_fuse_mount_archive(self):
  907. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  908. os.mkdir(mountpoint)
  909. self.cmd('init', self.repository_location)
  910. self.create_test_files()
  911. self.cmd('create', self.repository_location + '::archive', 'input')
  912. try:
  913. self.cmd('mount', self.repository_location + '::archive', mountpoint, fork=True)
  914. self.wait_for_mount(mountpoint)
  915. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'input'))
  916. finally:
  917. if sys.platform.startswith('linux'):
  918. os.system('fusermount -u ' + mountpoint)
  919. else:
  920. os.system('umount ' + mountpoint)
  921. os.rmdir(mountpoint)
  922. # Give the daemon some time to exit
  923. time.sleep(.2)
  924. def verify_aes_counter_uniqueness(self, method):
  925. seen = set() # Chunks already seen
  926. used = set() # counter values already used
  927. def verify_uniqueness():
  928. with Repository(self.repository_path) as repository:
  929. for key, _ in repository.open_index(repository.get_transaction_id()).iteritems():
  930. data = repository.get(key)
  931. hash = sha256(data).digest()
  932. if hash not in seen:
  933. seen.add(hash)
  934. num_blocks = num_aes_blocks(len(data) - 41)
  935. nonce = bytes_to_long(data[33:41])
  936. for counter in range(nonce, nonce + num_blocks):
  937. self.assert_not_in(counter, used)
  938. used.add(counter)
  939. self.create_test_files()
  940. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  941. self.cmd('init', '--encryption=' + method, self.repository_location)
  942. verify_uniqueness()
  943. self.cmd('create', self.repository_location + '::test', 'input')
  944. verify_uniqueness()
  945. self.cmd('create', self.repository_location + '::test.2', 'input')
  946. verify_uniqueness()
  947. self.cmd('delete', self.repository_location + '::test.2')
  948. verify_uniqueness()
  949. self.assert_equal(used, set(range(len(used))))
  950. def test_aes_counter_uniqueness_keyfile(self):
  951. self.verify_aes_counter_uniqueness('keyfile')
  952. def test_aes_counter_uniqueness_passphrase(self):
  953. self.verify_aes_counter_uniqueness('repokey')
  954. def test_debug_dump_archive_items(self):
  955. self.create_test_files()
  956. self.cmd('init', self.repository_location)
  957. self.cmd('create', self.repository_location + '::test', 'input')
  958. with changedir('output'):
  959. output = self.cmd('debug-dump-archive-items', self.repository_location + '::test')
  960. output_dir = sorted(os.listdir('output'))
  961. assert len(output_dir) > 0 and output_dir[0].startswith('000000_')
  962. assert 'Done.' in output
  963. def test_debug_put_get_delete_obj(self):
  964. self.cmd('init', self.repository_location)
  965. data = b'some data'
  966. hexkey = sha256(data).hexdigest()
  967. self.create_regular_file('file', contents=data)
  968. output = self.cmd('debug-put-obj', self.repository_location, 'input/file')
  969. assert hexkey in output
  970. output = self.cmd('debug-get-obj', self.repository_location, hexkey, 'output/file')
  971. assert hexkey in output
  972. with open('output/file', 'rb') as f:
  973. data_read = f.read()
  974. assert data == data_read
  975. output = self.cmd('debug-delete-obj', self.repository_location, hexkey)
  976. assert "deleted" in output
  977. output = self.cmd('debug-delete-obj', self.repository_location, hexkey)
  978. assert "not found" in output
  979. output = self.cmd('debug-delete-obj', self.repository_location, 'invalid')
  980. assert "is invalid" in output
  981. @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
  982. class ArchiverTestCaseBinary(ArchiverTestCase):
  983. EXE = 'borg.exe'
  984. FORK_DEFAULT = True
  985. class ArchiverCheckTestCase(ArchiverTestCaseBase):
  986. def setUp(self):
  987. super().setUp()
  988. with patch.object(ChunkBuffer, 'BUFFER_SIZE', 10):
  989. self.cmd('init', self.repository_location)
  990. self.create_src_archive('archive1')
  991. self.create_src_archive('archive2')
  992. def open_archive(self, name):
  993. repository = Repository(self.repository_path)
  994. with repository:
  995. manifest, key = Manifest.load(repository)
  996. archive = Archive(repository, key, manifest, name)
  997. return archive, repository
  998. def test_check_usage(self):
  999. output = self.cmd('check', '-v', self.repository_location, exit_code=0)
  1000. self.assert_in('Starting repository check', output)
  1001. self.assert_in('Starting archive consistency check', output)
  1002. output = self.cmd('check', '-v', '--repository-only', self.repository_location, exit_code=0)
  1003. self.assert_in('Starting repository check', output)
  1004. self.assert_not_in('Starting archive consistency check', output)
  1005. output = self.cmd('check', '-v', '--archives-only', self.repository_location, exit_code=0)
  1006. self.assert_not_in('Starting repository check', output)
  1007. self.assert_in('Starting archive consistency check', output)
  1008. output = self.cmd('check', '-v', '--archives-only', '--prefix=archive2', self.repository_location, exit_code=0)
  1009. self.assert_not_in('archive1', output)
  1010. def test_missing_file_chunk(self):
  1011. archive, repository = self.open_archive('archive1')
  1012. with repository:
  1013. for item in archive.iter_items():
  1014. if item[b'path'].endswith('testsuite/archiver.py'):
  1015. repository.delete(item[b'chunks'][-1][0])
  1016. break
  1017. repository.commit()
  1018. self.cmd('check', self.repository_location, exit_code=1)
  1019. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1020. self.cmd('check', self.repository_location, exit_code=0)
  1021. def test_missing_archive_item_chunk(self):
  1022. archive, repository = self.open_archive('archive1')
  1023. with repository:
  1024. repository.delete(archive.metadata[b'items'][-5])
  1025. repository.commit()
  1026. self.cmd('check', self.repository_location, exit_code=1)
  1027. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1028. self.cmd('check', self.repository_location, exit_code=0)
  1029. def test_missing_archive_metadata(self):
  1030. archive, repository = self.open_archive('archive1')
  1031. with repository:
  1032. repository.delete(archive.id)
  1033. repository.commit()
  1034. self.cmd('check', self.repository_location, exit_code=1)
  1035. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1036. self.cmd('check', self.repository_location, exit_code=0)
  1037. def test_missing_manifest(self):
  1038. archive, repository = self.open_archive('archive1')
  1039. with repository:
  1040. repository.delete(Manifest.MANIFEST_ID)
  1041. repository.commit()
  1042. self.cmd('check', self.repository_location, exit_code=1)
  1043. output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0)
  1044. self.assert_in('archive1', output)
  1045. self.assert_in('archive2', output)
  1046. self.cmd('check', self.repository_location, exit_code=0)
  1047. def test_extra_chunks(self):
  1048. self.cmd('check', self.repository_location, exit_code=0)
  1049. with Repository(self.repository_location) as repository:
  1050. repository.put(b'01234567890123456789012345678901', b'xxxx')
  1051. repository.commit()
  1052. self.cmd('check', self.repository_location, exit_code=1)
  1053. self.cmd('check', self.repository_location, exit_code=1)
  1054. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1055. self.cmd('check', self.repository_location, exit_code=0)
  1056. self.cmd('extract', '--dry-run', self.repository_location + '::archive1', exit_code=0)
  1057. class RemoteArchiverTestCase(ArchiverTestCase):
  1058. prefix = '__testsuite__:'
  1059. def test_remote_repo_restrict_to_path(self):
  1060. self.cmd('init', self.repository_location)
  1061. path_prefix = os.path.dirname(self.repository_path)
  1062. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo']):
  1063. self.assert_raises(PathNotAllowed, lambda: self.cmd('init', self.repository_location + '_1'))
  1064. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', path_prefix]):
  1065. self.cmd('init', self.repository_location + '_2')
  1066. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]):
  1067. self.cmd('init', self.repository_location + '_3')
  1068. # skip fuse tests here, they deadlock since this change in exec_cmd:
  1069. # -output = subprocess.check_output(borg + args, stderr=None)
  1070. # +output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT)
  1071. # this was introduced because some tests expect stderr contents to show up
  1072. # in "output" also. Also, the non-forking exec_cmd catches both, too.
  1073. @unittest.skip('deadlock issues')
  1074. def test_fuse_mount_repository(self):
  1075. pass
  1076. @unittest.skip('deadlock issues')
  1077. def test_fuse_mount_archive(self):
  1078. pass
  1079. @unittest.skip('only works locally')
  1080. def test_debug_put_get_delete_obj(self):
  1081. pass
  1082. def test_get_args():
  1083. archiver = Archiver()
  1084. # everything normal:
  1085. # first param is argv as produced by ssh forced command,
  1086. # second param is like from SSH_ORIGINAL_COMMAND env variable
  1087. args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
  1088. 'borg serve --info --umask=0027')
  1089. assert args.func == archiver.do_serve
  1090. assert args.restrict_to_paths == ['/p1', '/p2']
  1091. assert args.umask == 0o027
  1092. assert args.log_level == 'info'
  1093. # trying to cheat - break out of path restriction
  1094. args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
  1095. 'borg serve --restrict-to-path=/')
  1096. assert args.restrict_to_paths == ['/p1', '/p2']
  1097. # trying to cheat - try to execute different subcommand
  1098. args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
  1099. 'borg init /')
  1100. assert args.func == archiver.do_serve