archiver.py 86 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793
  1. from binascii import unhexlify, b2a_base64
  2. from configparser import ConfigParser
  3. import errno
  4. import os
  5. from datetime import datetime
  6. from datetime import timedelta
  7. from io import StringIO
  8. import random
  9. import stat
  10. import subprocess
  11. import sys
  12. import shutil
  13. import tempfile
  14. import time
  15. import unittest
  16. from unittest.mock import patch
  17. from hashlib import sha256
  18. import msgpack
  19. import pytest
  20. from .. import xattr, helpers
  21. from ..archive import Archive, ChunkBuffer, CHUNK_MAX_EXP, flags_noatime, flags_normal
  22. from ..archiver import Archiver
  23. from ..cache import Cache
  24. from ..crypto import bytes_to_long, num_aes_blocks
  25. from ..helpers import Manifest, PatternMatcher, parse_pattern, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, bin_to_hex, \
  26. get_security_dir
  27. from ..key import RepoKey, KeyfileKey, Passphrase, TAMRequiredError
  28. from ..keymanager import RepoIdMismatch, NotABorgKeyFile
  29. from ..remote import RemoteRepository, PathNotAllowed
  30. from ..repository import Repository
  31. from . import BaseTestCase, changedir, environment_variable, no_selinux
  32. from .platform import fakeroot_detected
  33. from . import key
  34. try:
  35. import llfuse
  36. has_llfuse = True or llfuse # avoids "unused import"
  37. except ImportError:
  38. has_llfuse = False
  39. has_lchflags = hasattr(os, 'lchflags')
  40. src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
  41. def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw):
  42. if fork:
  43. try:
  44. if exe is None:
  45. borg = (sys.executable, '-m', 'borg.archiver')
  46. elif isinstance(exe, str):
  47. borg = (exe, )
  48. elif not isinstance(exe, tuple):
  49. raise ValueError('exe must be None, a tuple or a str')
  50. output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT)
  51. ret = 0
  52. except subprocess.CalledProcessError as e:
  53. output = e.output
  54. ret = e.returncode
  55. return ret, os.fsdecode(output)
  56. else:
  57. stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr
  58. try:
  59. sys.stdin = StringIO()
  60. sys.stdout = sys.stderr = output = StringIO()
  61. if archiver is None:
  62. archiver = Archiver()
  63. archiver.exit_code = EXIT_SUCCESS
  64. helpers.exit_code = EXIT_SUCCESS
  65. args = archiver.parse_args(list(args))
  66. ret = archiver.run(args)
  67. return ret, output.getvalue()
  68. finally:
  69. sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr
  70. # check if the binary "borg.exe" is available (for local testing a symlink to virtualenv/bin/borg should do)
  71. try:
  72. exec_cmd('help', exe='borg.exe', fork=True)
  73. BORG_EXES = ['python', 'binary', ]
  74. except FileNotFoundError:
  75. BORG_EXES = ['python', ]
  76. @pytest.fixture(params=BORG_EXES)
  77. def cmd(request):
  78. if request.param == 'python':
  79. exe = None
  80. elif request.param == 'binary':
  81. exe = 'borg.exe'
  82. else:
  83. raise ValueError("param must be 'python' or 'binary'")
  84. def exec_fn(*args, **kw):
  85. return exec_cmd(*args, exe=exe, fork=True, **kw)
  86. return exec_fn
  87. def test_return_codes(cmd, tmpdir):
  88. repo = tmpdir.mkdir('repo')
  89. input = tmpdir.mkdir('input')
  90. output = tmpdir.mkdir('output')
  91. input.join('test_file').write('content')
  92. rc, out = cmd('init', '--encryption=none', '%s' % str(repo))
  93. assert rc == EXIT_SUCCESS
  94. rc, out = cmd('create', '%s::archive' % repo, str(input))
  95. assert rc == EXIT_SUCCESS
  96. with changedir(str(output)):
  97. rc, out = cmd('extract', '%s::archive' % repo)
  98. assert rc == EXIT_SUCCESS
  99. rc, out = cmd('extract', '%s::archive' % repo, 'does/not/match')
  100. assert rc == EXIT_WARNING # pattern did not match
  101. rc, out = cmd('create', '%s::archive' % repo, str(input))
  102. assert rc == EXIT_ERROR # duplicate archive name
  103. """
  104. test_disk_full is very slow and not recommended to be included in daily testing.
  105. for this test, an empty, writable 16MB filesystem mounted on DF_MOUNT is required.
  106. for speed and other reasons, it is recommended that the underlying block device is
  107. in RAM, not a magnetic or flash disk.
  108. assuming /tmp is a tmpfs (in memory filesystem), one can use this:
  109. dd if=/dev/zero of=/tmp/borg-disk bs=16M count=1
  110. mkfs.ext4 /tmp/borg-disk
  111. mkdir /tmp/borg-mount
  112. sudo mount /tmp/borg-disk /tmp/borg-mount
  113. if the directory does not exist, the test will be skipped.
  114. """
  115. DF_MOUNT = '/tmp/borg-mount'
  116. @pytest.mark.skipif(not os.path.exists(DF_MOUNT), reason="needs a 16MB fs mounted on %s" % DF_MOUNT)
  117. def test_disk_full(cmd):
  118. def make_files(dir, count, size, rnd=True):
  119. shutil.rmtree(dir, ignore_errors=True)
  120. os.mkdir(dir)
  121. if rnd:
  122. count = random.randint(1, count)
  123. if size > 1:
  124. size = random.randint(1, size)
  125. for i in range(count):
  126. fn = os.path.join(dir, "file%03d" % i)
  127. with open(fn, 'wb') as f:
  128. data = os.urandom(size)
  129. f.write(data)
  130. with environment_variable(BORG_CHECK_I_KNOW_WHAT_I_AM_DOING='YES'):
  131. mount = DF_MOUNT
  132. assert os.path.exists(mount)
  133. repo = os.path.join(mount, 'repo')
  134. input = os.path.join(mount, 'input')
  135. reserve = os.path.join(mount, 'reserve')
  136. for j in range(100):
  137. shutil.rmtree(repo, ignore_errors=True)
  138. shutil.rmtree(input, ignore_errors=True)
  139. # keep some space and some inodes in reserve that we can free up later:
  140. make_files(reserve, 80, 100000, rnd=False)
  141. rc, out = cmd('init', repo)
  142. if rc != EXIT_SUCCESS:
  143. print('init', rc, out)
  144. assert rc == EXIT_SUCCESS
  145. try:
  146. success, i = True, 0
  147. while success:
  148. i += 1
  149. try:
  150. make_files(input, 20, 200000)
  151. except OSError as err:
  152. if err.errno == errno.ENOSPC:
  153. # already out of space
  154. break
  155. raise
  156. try:
  157. rc, out = cmd('create', '%s::test%03d' % (repo, i), input)
  158. success = rc == EXIT_SUCCESS
  159. if not success:
  160. print('create', rc, out)
  161. finally:
  162. # make sure repo is not locked
  163. shutil.rmtree(os.path.join(repo, 'lock.exclusive'), ignore_errors=True)
  164. os.remove(os.path.join(repo, 'lock.roster'))
  165. finally:
  166. # now some error happened, likely we are out of disk space.
  167. # free some space so we can expect borg to be able to work normally:
  168. shutil.rmtree(reserve, ignore_errors=True)
  169. rc, out = cmd('list', repo)
  170. if rc != EXIT_SUCCESS:
  171. print('list', rc, out)
  172. rc, out = cmd('check', '--repair', repo)
  173. if rc != EXIT_SUCCESS:
  174. print('check', rc, out)
  175. assert rc == EXIT_SUCCESS
  176. class ArchiverTestCaseBase(BaseTestCase):
  177. EXE = None # python source based
  178. FORK_DEFAULT = False
  179. prefix = ''
  180. def setUp(self):
  181. os.environ['BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
  182. os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
  183. os.environ['BORG_PASSPHRASE'] = 'waytooeasyonlyfortests'
  184. self.archiver = not self.FORK_DEFAULT and Archiver() or None
  185. self.tmpdir = tempfile.mkdtemp()
  186. self.repository_path = os.path.join(self.tmpdir, 'repository')
  187. self.repository_location = self.prefix + self.repository_path
  188. self.input_path = os.path.join(self.tmpdir, 'input')
  189. self.output_path = os.path.join(self.tmpdir, 'output')
  190. self.keys_path = os.path.join(self.tmpdir, 'keys')
  191. self.cache_path = os.path.join(self.tmpdir, 'cache')
  192. self.exclude_file_path = os.path.join(self.tmpdir, 'excludes')
  193. self.patterns_file_path = os.path.join(self.tmpdir, 'patterns')
  194. os.environ['BORG_KEYS_DIR'] = self.keys_path
  195. os.environ['BORG_CACHE_DIR'] = self.cache_path
  196. os.mkdir(self.input_path)
  197. os.chmod(self.input_path, 0o777) # avoid troubles with fakeroot / FUSE
  198. os.mkdir(self.output_path)
  199. os.mkdir(self.keys_path)
  200. os.mkdir(self.cache_path)
  201. with open(self.exclude_file_path, 'wb') as fd:
  202. fd.write(b'input/file2\n# A comment line, then a blank line\n\n')
  203. with open(self.patterns_file_path, 'wb') as fd:
  204. fd.write(b'+input/file_important\n- input/file*\n# A comment line, then a blank line\n\n')
  205. self._old_wd = os.getcwd()
  206. os.chdir(self.tmpdir)
  207. def tearDown(self):
  208. os.chdir(self._old_wd)
  209. # note: ignore_errors=True as workaround for issue #862
  210. shutil.rmtree(self.tmpdir, ignore_errors=True)
  211. def cmd(self, *args, **kw):
  212. exit_code = kw.pop('exit_code', 0)
  213. fork = kw.pop('fork', None)
  214. if fork is None:
  215. fork = self.FORK_DEFAULT
  216. ret, output = exec_cmd(*args, fork=fork, exe=self.EXE, archiver=self.archiver, **kw)
  217. if ret != exit_code:
  218. print(output)
  219. self.assert_equal(ret, exit_code)
  220. return output
  221. def create_src_archive(self, name):
  222. self.cmd('create', '--compression=lz4', self.repository_location + '::' + name, src_dir)
  223. def open_archive(self, name):
  224. repository = Repository(self.repository_path, exclusive=True)
  225. with repository:
  226. manifest, key = Manifest.load(repository)
  227. archive = Archive(repository, key, manifest, name)
  228. return archive, repository
  229. class ArchiverTestCase(ArchiverTestCaseBase):
  230. def create_regular_file(self, name, size=0, contents=None):
  231. filename = os.path.join(self.input_path, name)
  232. if not os.path.exists(os.path.dirname(filename)):
  233. os.makedirs(os.path.dirname(filename))
  234. with open(filename, 'wb') as fd:
  235. if contents is None:
  236. contents = b'X' * size
  237. fd.write(contents)
  238. def create_test_files(self):
  239. """Create a minimal test case including all supported file types
  240. """
  241. # File
  242. self.create_regular_file('empty', size=0)
  243. # next code line raises OverflowError on 32bit cpu (raspberry pi 2):
  244. # 2600-01-01 > 2**64 ns
  245. # os.utime('input/empty', (19880895600, 19880895600))
  246. # thus, we better test with something not that far in future:
  247. # 2038-01-19 (1970 + 2^31 - 1 seconds) is the 32bit "deadline":
  248. os.utime('input/empty', (2**31 - 1, 2**31 - 1))
  249. self.create_regular_file('file1', size=1024 * 80)
  250. self.create_regular_file('flagfile', size=1024)
  251. # Directory
  252. self.create_regular_file('dir2/file2', size=1024 * 80)
  253. # File mode
  254. os.chmod('input/file1', 0o4755)
  255. # Hard link
  256. os.link(os.path.join(self.input_path, 'file1'),
  257. os.path.join(self.input_path, 'hardlink'))
  258. # Symlink
  259. os.symlink('somewhere', os.path.join(self.input_path, 'link1'))
  260. self.create_regular_file('fusexattr', size=1)
  261. if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path):
  262. # ironically, due to the way how fakeroot works, comparing fuse file xattrs to orig file xattrs
  263. # will FAIL if fakeroot supports xattrs, thus we only set the xattr if XATTR_FAKEROOT is False.
  264. # This is because fakeroot with xattr-support does not propagate xattrs of the underlying file
  265. # into "fakeroot space". Because the xattrs exposed by borgfs are these of an underlying file
  266. # (from fakeroots point of view) they are invisible to the test process inside the fakeroot.
  267. xattr.setxattr(os.path.join(self.input_path, 'fusexattr'), 'user.foo', b'bar')
  268. # XXX this always fails for me
  269. # ubuntu 14.04, on a TMP dir filesystem with user_xattr, using fakeroot
  270. # same for newer ubuntu and centos.
  271. # if this is supported just on specific platform, platform should be checked first,
  272. # so that the test setup for all tests using it does not fail here always for others.
  273. # xattr.setxattr(os.path.join(self.input_path, 'link1'), 'user.foo_symlink', b'bar_symlink', follow_symlinks=False)
  274. # FIFO node
  275. os.mkfifo(os.path.join(self.input_path, 'fifo1'))
  276. if has_lchflags:
  277. os.lchflags(os.path.join(self.input_path, 'flagfile'), stat.UF_NODUMP)
  278. try:
  279. # Block device
  280. os.mknod('input/bdev', 0o600 | stat.S_IFBLK, os.makedev(10, 20))
  281. # Char device
  282. os.mknod('input/cdev', 0o600 | stat.S_IFCHR, os.makedev(30, 40))
  283. # File mode
  284. os.chmod('input/dir2', 0o555) # if we take away write perms, we need root to remove contents
  285. # File owner
  286. os.chown('input/file1', 100, 200) # raises OSError invalid argument on cygwin
  287. have_root = True # we have (fake)root
  288. except PermissionError:
  289. have_root = False
  290. except OSError as e:
  291. # Note: ENOSYS "Function not implemented" happens as non-root on Win 10 Linux Subsystem.
  292. if e.errno not in (errno.EINVAL, errno.ENOSYS):
  293. raise
  294. have_root = False
  295. return have_root
  296. def test_basic_functionality(self):
  297. have_root = self.create_test_files()
  298. self.cmd('init', self.repository_location)
  299. self.cmd('create', self.repository_location + '::test', 'input')
  300. self.cmd('create', '--stats', self.repository_location + '::test.2', 'input')
  301. with changedir('output'):
  302. self.cmd('extract', self.repository_location + '::test')
  303. list_output = self.cmd('list', '--short', self.repository_location)
  304. self.assert_in('test', list_output)
  305. self.assert_in('test.2', list_output)
  306. expected = [
  307. 'input',
  308. 'input/bdev',
  309. 'input/cdev',
  310. 'input/dir2',
  311. 'input/dir2/file2',
  312. 'input/empty',
  313. 'input/fifo1',
  314. 'input/file1',
  315. 'input/flagfile',
  316. 'input/hardlink',
  317. 'input/link1',
  318. ]
  319. if not have_root:
  320. # we could not create these device files without (fake)root
  321. expected.remove('input/bdev')
  322. expected.remove('input/cdev')
  323. if has_lchflags:
  324. # remove the file we did not backup, so input and output become equal
  325. expected.remove('input/flagfile') # this file is UF_NODUMP
  326. os.remove(os.path.join('input', 'flagfile'))
  327. list_output = self.cmd('list', '--short', self.repository_location + '::test')
  328. for name in expected:
  329. self.assert_in(name, list_output)
  330. self.assert_dirs_equal('input', 'output/input')
  331. info_output = self.cmd('info', self.repository_location + '::test')
  332. item_count = 4 if has_lchflags else 5 # one file is UF_NODUMP
  333. self.assert_in('Number of files: %d' % item_count, info_output)
  334. shutil.rmtree(self.cache_path)
  335. with environment_variable(BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='yes'):
  336. info_output2 = self.cmd('info', self.repository_location + '::test')
  337. def filter(output):
  338. # filter for interesting "info" output, ignore cache rebuilding related stuff
  339. prefixes = ['Name:', 'Fingerprint:', 'Number of files:', 'This archive:',
  340. 'All archives:', 'Chunk index:', ]
  341. result = []
  342. for line in output.splitlines():
  343. for prefix in prefixes:
  344. if line.startswith(prefix):
  345. result.append(line)
  346. return '\n'.join(result)
  347. # the interesting parts of info_output2 and info_output should be same
  348. self.assert_equal(filter(info_output), filter(info_output2))
  349. # Search for O_NOATIME there: https://www.gnu.org/software/hurd/contributing.html - we just
  350. # skip the test on Hurd, it is not critical anyway, just testing a performance optimization.
  351. @pytest.mark.skipif(sys.platform == 'gnu0', reason="O_NOATIME is strangely broken on GNU Hurd")
  352. def test_atime(self):
  353. def has_noatime(some_file):
  354. atime_before = os.stat(some_file).st_atime_ns
  355. try:
  356. os.close(os.open(some_file, flags_noatime))
  357. except PermissionError:
  358. return False
  359. else:
  360. atime_after = os.stat(some_file).st_atime_ns
  361. noatime_used = flags_noatime != flags_normal
  362. return noatime_used and atime_before == atime_after
  363. self.create_test_files()
  364. atime, mtime = 123456780, 234567890
  365. have_noatime = has_noatime('input/file1')
  366. os.utime('input/file1', (atime, mtime))
  367. self.cmd('init', self.repository_location)
  368. self.cmd('create', self.repository_location + '::test', 'input')
  369. with changedir('output'):
  370. self.cmd('extract', self.repository_location + '::test')
  371. sti = os.stat('input/file1')
  372. sto = os.stat('output/input/file1')
  373. assert sti.st_mtime_ns == sto.st_mtime_ns == mtime * 1e9
  374. if have_noatime:
  375. assert sti.st_atime_ns == sto.st_atime_ns == atime * 1e9
  376. else:
  377. # it touched the input file's atime while backing it up
  378. assert sto.st_atime_ns == atime * 1e9
  379. def _extract_repository_id(self, path):
  380. with Repository(self.repository_path) as repository:
  381. return repository.id
  382. def _set_repository_id(self, path, id):
  383. config = ConfigParser(interpolation=None)
  384. config.read(os.path.join(path, 'config'))
  385. config.set('repository', 'id', bin_to_hex(id))
  386. with open(os.path.join(path, 'config'), 'w') as fd:
  387. config.write(fd)
  388. with Repository(self.repository_path) as repository:
  389. return repository.id
  390. def test_sparse_file(self):
  391. def is_sparse(fn, total_size, hole_size):
  392. st = os.stat(fn)
  393. assert st.st_size == total_size
  394. sparse = True
  395. if sparse and hasattr(st, 'st_blocks') and st.st_blocks * 512 >= st.st_size:
  396. sparse = False
  397. if sparse and hasattr(os, 'SEEK_HOLE') and hasattr(os, 'SEEK_DATA'):
  398. with open(fn, 'rb') as fd:
  399. # only check if the first hole is as expected, because the 2nd hole check
  400. # is problematic on xfs due to its "dynamic speculative EOF preallocation
  401. try:
  402. if fd.seek(0, os.SEEK_HOLE) != 0:
  403. sparse = False
  404. if fd.seek(0, os.SEEK_DATA) != hole_size:
  405. sparse = False
  406. except OSError:
  407. # OS/FS does not really support SEEK_HOLE/SEEK_DATA
  408. sparse = False
  409. return sparse
  410. filename = os.path.join(self.input_path, 'sparse')
  411. content = b'foobar'
  412. hole_size = 5 * (1 << CHUNK_MAX_EXP) # 5 full chunker buffers
  413. total_size = hole_size + len(content) + hole_size
  414. with open(filename, 'wb') as fd:
  415. # create a file that has a hole at the beginning and end (if the
  416. # OS and filesystem supports sparse files)
  417. fd.seek(hole_size, 1)
  418. fd.write(content)
  419. fd.seek(hole_size, 1)
  420. pos = fd.tell()
  421. fd.truncate(pos)
  422. # we first check if we could create a sparse input file:
  423. sparse_support = is_sparse(filename, total_size, hole_size)
  424. if sparse_support:
  425. # we could create a sparse input file, so creating a backup of it and
  426. # extracting it again (as sparse) should also work:
  427. self.cmd('init', self.repository_location)
  428. self.cmd('create', self.repository_location + '::test', 'input')
  429. with changedir(self.output_path):
  430. self.cmd('extract', '--sparse', self.repository_location + '::test')
  431. self.assert_dirs_equal('input', 'output/input')
  432. filename = os.path.join(self.output_path, 'input', 'sparse')
  433. with open(filename, 'rb') as fd:
  434. # check if file contents are as expected
  435. self.assert_equal(fd.read(hole_size), b'\0' * hole_size)
  436. self.assert_equal(fd.read(len(content)), content)
  437. self.assert_equal(fd.read(hole_size), b'\0' * hole_size)
  438. self.assert_true(is_sparse(filename, total_size, hole_size))
  439. def test_unusual_filenames(self):
  440. filenames = ['normal', 'with some blanks', '(with_parens)', ]
  441. for filename in filenames:
  442. filename = os.path.join(self.input_path, filename)
  443. with open(filename, 'wb'):
  444. pass
  445. self.cmd('init', self.repository_location)
  446. self.cmd('create', self.repository_location + '::test', 'input')
  447. for filename in filenames:
  448. with changedir('output'):
  449. self.cmd('extract', self.repository_location + '::test', os.path.join('input', filename))
  450. assert os.path.exists(os.path.join('output', 'input', filename))
  451. def test_repository_swap_detection(self):
  452. self.create_test_files()
  453. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  454. self.cmd('init', '--encryption=repokey', self.repository_location)
  455. repository_id = self._extract_repository_id(self.repository_path)
  456. self.cmd('create', self.repository_location + '::test', 'input')
  457. shutil.rmtree(self.repository_path)
  458. self.cmd('init', '--encryption=none', self.repository_location)
  459. self._set_repository_id(self.repository_path, repository_id)
  460. self.assert_equal(repository_id, self._extract_repository_id(self.repository_path))
  461. if self.FORK_DEFAULT:
  462. self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR)
  463. else:
  464. self.assert_raises(Cache.EncryptionMethodMismatch, lambda: self.cmd('create', self.repository_location + '::test.2', 'input'))
  465. def test_repository_swap_detection2(self):
  466. self.create_test_files()
  467. self.cmd('init', '--encryption=none', self.repository_location + '_unencrypted')
  468. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  469. self.cmd('init', '--encryption=repokey', self.repository_location + '_encrypted')
  470. self.cmd('create', self.repository_location + '_encrypted::test', 'input')
  471. shutil.rmtree(self.repository_path + '_encrypted')
  472. os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted')
  473. if self.FORK_DEFAULT:
  474. self.cmd('create', self.repository_location + '_encrypted::test.2', 'input', exit_code=EXIT_ERROR)
  475. else:
  476. self.assert_raises(Cache.RepositoryAccessAborted, lambda: self.cmd('create', self.repository_location + '_encrypted::test.2', 'input'))
  477. def test_strip_components(self):
  478. self.cmd('init', self.repository_location)
  479. self.create_regular_file('dir/file')
  480. self.cmd('create', self.repository_location + '::test', 'input')
  481. with changedir('output'):
  482. self.cmd('extract', self.repository_location + '::test', '--strip-components', '3')
  483. self.assert_true(not os.path.exists('file'))
  484. with self.assert_creates_file('file'):
  485. self.cmd('extract', self.repository_location + '::test', '--strip-components', '2')
  486. with self.assert_creates_file('dir/file'):
  487. self.cmd('extract', self.repository_location + '::test', '--strip-components', '1')
  488. with self.assert_creates_file('input/dir/file'):
  489. self.cmd('extract', self.repository_location + '::test', '--strip-components', '0')
  490. def test_extract_include_exclude(self):
  491. self.cmd('init', self.repository_location)
  492. self.create_regular_file('file1', size=1024 * 80)
  493. self.create_regular_file('file2', size=1024 * 80)
  494. self.create_regular_file('file3', size=1024 * 80)
  495. self.create_regular_file('file4', size=1024 * 80)
  496. self.cmd('create', '--exclude=input/file4', self.repository_location + '::test', 'input')
  497. with changedir('output'):
  498. self.cmd('extract', self.repository_location + '::test', 'input/file1', )
  499. self.assert_equal(sorted(os.listdir('output/input')), ['file1'])
  500. with changedir('output'):
  501. self.cmd('extract', '--exclude=input/file2', self.repository_location + '::test')
  502. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  503. with changedir('output'):
  504. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  505. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  506. def test_extract_include_exclude_regex(self):
  507. self.cmd('init', self.repository_location)
  508. self.create_regular_file('file1', size=1024 * 80)
  509. self.create_regular_file('file2', size=1024 * 80)
  510. self.create_regular_file('file3', size=1024 * 80)
  511. self.create_regular_file('file4', size=1024 * 80)
  512. self.create_regular_file('file333', size=1024 * 80)
  513. # Create with regular expression exclusion for file4
  514. self.cmd('create', '--exclude=re:input/file4$', self.repository_location + '::test', 'input')
  515. with changedir('output'):
  516. self.cmd('extract', self.repository_location + '::test')
  517. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333'])
  518. shutil.rmtree('output/input')
  519. # Extract with regular expression exclusion
  520. with changedir('output'):
  521. self.cmd('extract', '--exclude=re:file3+', self.repository_location + '::test')
  522. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2'])
  523. shutil.rmtree('output/input')
  524. # Combine --exclude with fnmatch and regular expression
  525. with changedir('output'):
  526. self.cmd('extract', '--exclude=input/file2', '--exclude=re:file[01]', self.repository_location + '::test')
  527. self.assert_equal(sorted(os.listdir('output/input')), ['file3', 'file333'])
  528. shutil.rmtree('output/input')
  529. # Combine --exclude-from and regular expression exclusion
  530. with changedir('output'):
  531. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, '--exclude=re:file1',
  532. '--exclude=re:file(\\d)\\1\\1$', self.repository_location + '::test')
  533. self.assert_equal(sorted(os.listdir('output/input')), ['file3'])
  534. def test_extract_include_exclude_regex_from_file(self):
  535. self.cmd('init', self.repository_location)
  536. self.create_regular_file('file1', size=1024 * 80)
  537. self.create_regular_file('file2', size=1024 * 80)
  538. self.create_regular_file('file3', size=1024 * 80)
  539. self.create_regular_file('file4', size=1024 * 80)
  540. self.create_regular_file('file333', size=1024 * 80)
  541. self.create_regular_file('aa:something', size=1024 * 80)
  542. # Create while excluding using mixed pattern styles
  543. with open(self.exclude_file_path, 'wb') as fd:
  544. fd.write(b're:input/file4$\n')
  545. fd.write(b'fm:*aa:*thing\n')
  546. self.cmd('create', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test', 'input')
  547. with changedir('output'):
  548. self.cmd('extract', self.repository_location + '::test')
  549. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333'])
  550. shutil.rmtree('output/input')
  551. # Exclude using regular expression
  552. with open(self.exclude_file_path, 'wb') as fd:
  553. fd.write(b're:file3+\n')
  554. with changedir('output'):
  555. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  556. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2'])
  557. shutil.rmtree('output/input')
  558. # Mixed exclude pattern styles
  559. with open(self.exclude_file_path, 'wb') as fd:
  560. fd.write(b're:file(\\d)\\1\\1$\n')
  561. fd.write(b'fm:nothingwillmatchthis\n')
  562. fd.write(b'*/file1\n')
  563. fd.write(b're:file2$\n')
  564. with changedir('output'):
  565. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  566. self.assert_equal(sorted(os.listdir('output/input')), ['file3'])
  567. def test_extract_with_pattern(self):
  568. self.cmd("init", self.repository_location)
  569. self.create_regular_file("file1", size=1024 * 80)
  570. self.create_regular_file("file2", size=1024 * 80)
  571. self.create_regular_file("file3", size=1024 * 80)
  572. self.create_regular_file("file4", size=1024 * 80)
  573. self.create_regular_file("file333", size=1024 * 80)
  574. self.cmd("create", self.repository_location + "::test", "input")
  575. # Extract everything with regular expression
  576. with changedir("output"):
  577. self.cmd("extract", self.repository_location + "::test", "re:.*")
  578. self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file3", "file333", "file4"])
  579. shutil.rmtree("output/input")
  580. # Extract with pattern while also excluding files
  581. with changedir("output"):
  582. self.cmd("extract", "--exclude=re:file[34]$", self.repository_location + "::test", r"re:file\d$")
  583. self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2"])
  584. shutil.rmtree("output/input")
  585. # Combine --exclude with pattern for extraction
  586. with changedir("output"):
  587. self.cmd("extract", "--exclude=input/file1", self.repository_location + "::test", "re:file[12]$")
  588. self.assert_equal(sorted(os.listdir("output/input")), ["file2"])
  589. shutil.rmtree("output/input")
  590. # Multiple pattern
  591. with changedir("output"):
  592. self.cmd("extract", self.repository_location + "::test", "fm:input/file1", "fm:*file33*", "input/file2")
  593. self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file333"])
  594. def test_create_without_root(self):
  595. """test create without a root"""
  596. self.cmd('init', self.repository_location)
  597. args = ['create', self.repository_location + '::test']
  598. if self.FORK_DEFAULT:
  599. self.cmd(*args, exit_code=2)
  600. else:
  601. self.assert_raises(SystemExit, lambda: self.cmd(*args))
  602. def test_create_pattern_root(self):
  603. """test create with only a root pattern"""
  604. self.cmd('init', self.repository_location)
  605. self.create_regular_file('file1', size=1024 * 80)
  606. self.create_regular_file('file2', size=1024 * 80)
  607. output = self.cmd('create', '-v', '--list', '--pattern=R input', self.repository_location + '::test')
  608. self.assert_in("A input/file1", output)
  609. self.assert_in("A input/file2", output)
  610. def test_create_pattern(self):
  611. """test file patterns during create"""
  612. self.cmd('init', self.repository_location)
  613. self.create_regular_file('file1', size=1024 * 80)
  614. self.create_regular_file('file2', size=1024 * 80)
  615. self.create_regular_file('file_important', size=1024 * 80)
  616. output = self.cmd('create', '-v', '--list',
  617. '--pattern=+input/file_important', '--pattern=-input/file*',
  618. self.repository_location + '::test', 'input')
  619. self.assert_in("A input/file_important", output)
  620. self.assert_not_in('file1', output)
  621. self.assert_not_in('file2', output)
  622. def test_create_pattern_file(self):
  623. """test file patterns during create"""
  624. self.cmd('init', self.repository_location)
  625. self.create_regular_file('file1', size=1024 * 80)
  626. self.create_regular_file('file2', size=1024 * 80)
  627. self.create_regular_file('otherfile', size=1024 * 80)
  628. self.create_regular_file('file_important', size=1024 * 80)
  629. output = self.cmd('create', '-v', '--list',
  630. '--pattern=-input/otherfile', '--patterns-from=' + self.patterns_file_path,
  631. self.repository_location + '::test', 'input')
  632. self.assert_in("A input/file_important", output)
  633. self.assert_not_in('file1', output)
  634. self.assert_not_in('file2', output)
  635. self.assert_not_in('otherfile', output)
  636. def test_extract_pattern_opt(self):
  637. self.cmd('init', self.repository_location)
  638. self.create_regular_file('file1', size=1024 * 80)
  639. self.create_regular_file('file2', size=1024 * 80)
  640. self.create_regular_file('file_important', size=1024 * 80)
  641. self.cmd('create', self.repository_location + '::test', 'input')
  642. with changedir('output'):
  643. self.cmd('extract',
  644. '--pattern=+input/file_important', '--pattern=-input/file*',
  645. self.repository_location + '::test')
  646. self.assert_equal(sorted(os.listdir('output/input')), ['file_important'])
  647. def test_exclude_caches(self):
  648. self.cmd('init', self.repository_location)
  649. self.create_regular_file('file1', size=1024 * 80)
  650. self.create_regular_file('cache1/CACHEDIR.TAG', contents=b'Signature: 8a477f597d28d172789f06886806bc55 extra stuff')
  651. self.create_regular_file('cache2/CACHEDIR.TAG', contents=b'invalid signature')
  652. self.cmd('create', '--exclude-caches', self.repository_location + '::test', 'input')
  653. with changedir('output'):
  654. self.cmd('extract', self.repository_location + '::test')
  655. self.assert_equal(sorted(os.listdir('output/input')), ['cache2', 'file1'])
  656. self.assert_equal(sorted(os.listdir('output/input/cache2')), ['CACHEDIR.TAG'])
  657. def test_exclude_tagged(self):
  658. self.cmd('init', self.repository_location)
  659. self.create_regular_file('file1', size=1024 * 80)
  660. self.create_regular_file('tagged1/.NOBACKUP')
  661. self.create_regular_file('tagged2/00-NOBACKUP')
  662. self.create_regular_file('tagged3/.NOBACKUP/file2')
  663. self.cmd('create', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP', self.repository_location + '::test', 'input')
  664. with changedir('output'):
  665. self.cmd('extract', self.repository_location + '::test')
  666. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'tagged3'])
  667. def test_exclude_keep_tagged(self):
  668. self.cmd('init', self.repository_location)
  669. self.create_regular_file('file0', size=1024)
  670. self.create_regular_file('tagged1/.NOBACKUP1')
  671. self.create_regular_file('tagged1/file1', size=1024)
  672. self.create_regular_file('tagged2/.NOBACKUP2')
  673. self.create_regular_file('tagged2/file2', size=1024)
  674. self.create_regular_file('tagged3/CACHEDIR.TAG', contents=b'Signature: 8a477f597d28d172789f06886806bc55 extra stuff')
  675. self.create_regular_file('tagged3/file3', size=1024)
  676. self.create_regular_file('taggedall/.NOBACKUP1')
  677. self.create_regular_file('taggedall/.NOBACKUP2')
  678. self.create_regular_file('taggedall/CACHEDIR.TAG', contents=b'Signature: 8a477f597d28d172789f06886806bc55 extra stuff')
  679. self.create_regular_file('taggedall/file4', size=1024)
  680. self.cmd('create', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2',
  681. '--exclude-caches', '--keep-tag-files', self.repository_location + '::test', 'input')
  682. with changedir('output'):
  683. self.cmd('extract', self.repository_location + '::test')
  684. self.assert_equal(sorted(os.listdir('output/input')), ['file0', 'tagged1', 'tagged2', 'tagged3', 'taggedall'])
  685. self.assert_equal(os.listdir('output/input/tagged1'), ['.NOBACKUP1'])
  686. self.assert_equal(os.listdir('output/input/tagged2'), ['.NOBACKUP2'])
  687. self.assert_equal(os.listdir('output/input/tagged3'), ['CACHEDIR.TAG'])
  688. self.assert_equal(sorted(os.listdir('output/input/taggedall')),
  689. ['.NOBACKUP1', '.NOBACKUP2', 'CACHEDIR.TAG', ])
  690. @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='Linux capabilities test, requires fakeroot >= 1.20.2')
  691. def test_extract_capabilities(self):
  692. fchown = os.fchown
  693. # We need to manually patch chown to get the behaviour Linux has, since fakeroot does not
  694. # accurately model the interaction of chown(2) and Linux capabilities, i.e. it does not remove them.
  695. def patched_fchown(fd, uid, gid):
  696. xattr.setxattr(fd, 'security.capability', None, follow_symlinks=False)
  697. fchown(fd, uid, gid)
  698. # The capability descriptor used here is valid and taken from a /usr/bin/ping
  699. capabilities = b'\x01\x00\x00\x02\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
  700. self.create_regular_file('file')
  701. xattr.setxattr('input/file', 'security.capability', capabilities)
  702. self.cmd('init', self.repository_location)
  703. self.cmd('create', self.repository_location + '::test', 'input')
  704. with changedir('output'):
  705. with patch.object(os, 'fchown', patched_fchown):
  706. self.cmd('extract', self.repository_location + '::test')
  707. assert xattr.getxattr('input/file', 'security.capability') == capabilities
  708. @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='xattr not supported on this system or on this version of'
  709. 'fakeroot')
  710. def test_extract_big_xattrs(self):
  711. def patched_setxattr(*args, **kwargs):
  712. raise OSError(errno.E2BIG, 'E2BIG')
  713. self.create_regular_file('file')
  714. xattr.setxattr('input/file', 'attribute', 'value')
  715. self.cmd('init', self.repository_location, '-e' 'none')
  716. self.cmd('create', self.repository_location + '::test', 'input')
  717. with changedir('output'):
  718. with patch.object(xattr, 'setxattr', patched_setxattr):
  719. out = self.cmd('extract', self.repository_location + '::test', exit_code=EXIT_WARNING)
  720. assert out == (os.path.abspath('input/file') + ': Value or key of extended attribute attribute is too big'
  721. 'for this filesystem\n')
  722. def test_path_normalization(self):
  723. self.cmd('init', self.repository_location)
  724. self.create_regular_file('dir1/dir2/file', size=1024 * 80)
  725. with changedir('input/dir1/dir2'):
  726. self.cmd('create', self.repository_location + '::test', '../../../input/dir1/../dir1/dir2/..')
  727. output = self.cmd('list', self.repository_location + '::test')
  728. self.assert_not_in('..', output)
  729. self.assert_in(' input/dir1/dir2/file', output)
  730. def test_exclude_normalization(self):
  731. self.cmd('init', self.repository_location)
  732. self.create_regular_file('file1', size=1024 * 80)
  733. self.create_regular_file('file2', size=1024 * 80)
  734. with changedir('input'):
  735. self.cmd('create', '--exclude=file1', self.repository_location + '::test1', '.')
  736. with changedir('output'):
  737. self.cmd('extract', self.repository_location + '::test1')
  738. self.assert_equal(sorted(os.listdir('output')), ['file2'])
  739. with changedir('input'):
  740. self.cmd('create', '--exclude=./file1', self.repository_location + '::test2', '.')
  741. with changedir('output'):
  742. self.cmd('extract', self.repository_location + '::test2')
  743. self.assert_equal(sorted(os.listdir('output')), ['file2'])
  744. self.cmd('create', '--exclude=input/./file1', self.repository_location + '::test3', 'input')
  745. with changedir('output'):
  746. self.cmd('extract', self.repository_location + '::test3')
  747. self.assert_equal(sorted(os.listdir('output/input')), ['file2'])
  748. def test_repeated_files(self):
  749. self.create_regular_file('file1', size=1024 * 80)
  750. self.cmd('init', self.repository_location)
  751. self.cmd('create', self.repository_location + '::test', 'input', 'input')
  752. def test_overwrite(self):
  753. self.create_regular_file('file1', size=1024 * 80)
  754. self.create_regular_file('dir2/file2', size=1024 * 80)
  755. self.cmd('init', self.repository_location)
  756. self.cmd('create', self.repository_location + '::test', 'input')
  757. # Overwriting regular files and directories should be supported
  758. os.mkdir('output/input')
  759. os.mkdir('output/input/file1')
  760. os.mkdir('output/input/dir2')
  761. with changedir('output'):
  762. self.cmd('extract', self.repository_location + '::test')
  763. self.assert_dirs_equal('input', 'output/input')
  764. # But non-empty dirs should fail
  765. os.unlink('output/input/file1')
  766. os.mkdir('output/input/file1')
  767. os.mkdir('output/input/file1/dir')
  768. with changedir('output'):
  769. self.cmd('extract', self.repository_location + '::test', exit_code=1)
  770. def test_rename(self):
  771. self.create_regular_file('file1', size=1024 * 80)
  772. self.create_regular_file('dir2/file2', size=1024 * 80)
  773. self.cmd('init', self.repository_location)
  774. self.cmd('create', self.repository_location + '::test', 'input')
  775. self.cmd('create', self.repository_location + '::test.2', 'input')
  776. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  777. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  778. self.cmd('rename', self.repository_location + '::test', 'test.3')
  779. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  780. self.cmd('rename', self.repository_location + '::test.2', 'test.4')
  781. self.cmd('extract', '--dry-run', self.repository_location + '::test.3')
  782. self.cmd('extract', '--dry-run', self.repository_location + '::test.4')
  783. # Make sure both archives have been renamed
  784. with Repository(self.repository_path) as repository:
  785. manifest, key = Manifest.load(repository)
  786. self.assert_equal(len(manifest.archives), 2)
  787. self.assert_in('test.3', manifest.archives)
  788. self.assert_in('test.4', manifest.archives)
  789. def test_delete(self):
  790. self.create_regular_file('file1', size=1024 * 80)
  791. self.create_regular_file('dir2/file2', size=1024 * 80)
  792. self.cmd('init', self.repository_location)
  793. self.cmd('create', self.repository_location + '::test', 'input')
  794. self.cmd('create', self.repository_location + '::test.2', 'input')
  795. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  796. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  797. self.cmd('delete', self.repository_location + '::test')
  798. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  799. self.cmd('delete', '--stats', self.repository_location + '::test.2')
  800. # Make sure all data except the manifest has been deleted
  801. with Repository(self.repository_path) as repository:
  802. self.assert_equal(len(repository), 1)
  803. def test_delete_repo(self):
  804. self.create_regular_file('file1', size=1024 * 80)
  805. self.create_regular_file('dir2/file2', size=1024 * 80)
  806. self.cmd('init', self.repository_location)
  807. self.cmd('create', self.repository_location + '::test', 'input')
  808. self.cmd('create', self.repository_location + '::test.2', 'input')
  809. os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'no'
  810. self.cmd('delete', self.repository_location, exit_code=2)
  811. assert os.path.exists(self.repository_path)
  812. os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
  813. self.cmd('delete', self.repository_location)
  814. # Make sure the repo is gone
  815. self.assertFalse(os.path.exists(self.repository_path))
  816. def test_corrupted_repository(self):
  817. self.cmd('init', self.repository_location)
  818. self.create_src_archive('test')
  819. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  820. self.cmd('check', self.repository_location)
  821. name = sorted(os.listdir(os.path.join(self.tmpdir, 'repository', 'data', '0')), reverse=True)[0]
  822. with open(os.path.join(self.tmpdir, 'repository', 'data', '0', name), 'r+b') as fd:
  823. fd.seek(100)
  824. fd.write(b'XXXX')
  825. self.cmd('check', self.repository_location, exit_code=1)
  826. # we currently need to be able to create a lock directory inside the repo:
  827. @pytest.mark.xfail(reason="we need to be able to create the lock directory inside the repo")
  828. def test_readonly_repository(self):
  829. self.cmd('init', self.repository_location)
  830. self.create_src_archive('test')
  831. os.system('chmod -R ugo-w ' + self.repository_path)
  832. try:
  833. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  834. finally:
  835. # Restore permissions so shutil.rmtree is able to delete it
  836. os.system('chmod -R u+w ' + self.repository_path)
  837. def test_umask(self):
  838. self.create_regular_file('file1', size=1024 * 80)
  839. self.cmd('init', self.repository_location)
  840. self.cmd('create', self.repository_location + '::test', 'input')
  841. mode = os.stat(self.repository_path).st_mode
  842. self.assertEqual(stat.S_IMODE(mode), 0o700)
  843. def test_create_dry_run(self):
  844. self.cmd('init', self.repository_location)
  845. self.cmd('create', '--dry-run', self.repository_location + '::test', 'input')
  846. # Make sure no archive has been created
  847. with Repository(self.repository_path) as repository:
  848. manifest, key = Manifest.load(repository)
  849. self.assert_equal(len(manifest.archives), 0)
  850. def test_progress(self):
  851. self.create_regular_file('file1', size=1024 * 80)
  852. self.cmd('init', self.repository_location)
  853. # progress forced on
  854. output = self.cmd('create', '--progress', self.repository_location + '::test4', 'input')
  855. self.assert_in("\r", output)
  856. # progress forced off
  857. output = self.cmd('create', self.repository_location + '::test5', 'input')
  858. self.assert_not_in("\r", output)
  859. def test_file_status(self):
  860. """test that various file status show expected results
  861. clearly incomplete: only tests for the weird "unchanged" status for now"""
  862. now = time.time()
  863. self.create_regular_file('file1', size=1024 * 80)
  864. os.utime('input/file1', (now - 5, now - 5)) # 5 seconds ago
  865. self.create_regular_file('file2', size=1024 * 80)
  866. self.cmd('init', self.repository_location)
  867. output = self.cmd('create', '-v', '--list', self.repository_location + '::test', 'input')
  868. self.assert_in("A input/file1", output)
  869. self.assert_in("A input/file2", output)
  870. # should find first file as unmodified
  871. output = self.cmd('create', '-v', '--list', self.repository_location + '::test1', 'input')
  872. self.assert_in("U input/file1", output)
  873. # this is expected, although surprising, for why, see:
  874. # https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file
  875. self.assert_in("A input/file2", output)
  876. def test_create_topical(self):
  877. now = time.time()
  878. self.create_regular_file('file1', size=1024 * 80)
  879. os.utime('input/file1', (now-5, now-5))
  880. self.create_regular_file('file2', size=1024 * 80)
  881. self.cmd('init', self.repository_location)
  882. # no listing by default
  883. output = self.cmd('create', self.repository_location + '::test', 'input')
  884. self.assert_not_in('file1', output)
  885. # shouldn't be listed even if unchanged
  886. output = self.cmd('create', self.repository_location + '::test0', 'input')
  887. self.assert_not_in('file1', output)
  888. # should list the file as unchanged
  889. output = self.cmd('create', '-v', '--list', '--filter=U', self.repository_location + '::test1', 'input')
  890. self.assert_in('file1', output)
  891. # should *not* list the file as changed
  892. output = self.cmd('create', '-v', '--filter=AM', self.repository_location + '::test2', 'input')
  893. self.assert_not_in('file1', output)
  894. # change the file
  895. self.create_regular_file('file1', size=1024 * 100)
  896. # should list the file as changed
  897. output = self.cmd('create', '-v', '--list', '--filter=AM', self.repository_location + '::test3', 'input')
  898. self.assert_in('file1', output)
  899. def test_create_read_special_broken_symlink(self):
  900. os.symlink('somewhere doesnt exist', os.path.join(self.input_path, 'link'))
  901. self.cmd('init', self.repository_location)
  902. archive = self.repository_location + '::test'
  903. self.cmd('create', '--read-special', archive, 'input')
  904. output = self.cmd('list', archive)
  905. assert 'input/link -> somewhere doesnt exist' in output
  906. # def test_cmdline_compatibility(self):
  907. # self.create_regular_file('file1', size=1024 * 80)
  908. # self.cmd('init', self.repository_location)
  909. # self.cmd('create', self.repository_location + '::test', 'input')
  910. # output = self.cmd('foo', self.repository_location, '--old')
  911. # self.assert_in('"--old" has been deprecated. Use "--new" instead', output)
  912. def test_prune_repository(self):
  913. self.cmd('init', self.repository_location)
  914. self.cmd('create', self.repository_location + '::test1', src_dir)
  915. self.cmd('create', self.repository_location + '::test2', src_dir)
  916. # these are not really a checkpoints, but they look like some:
  917. self.cmd('create', self.repository_location + '::test3.checkpoint', src_dir)
  918. self.cmd('create', self.repository_location + '::test3.checkpoint.1', src_dir)
  919. output = self.cmd('prune', '-v', '--list', '--dry-run', self.repository_location, '--keep-daily=2')
  920. self.assert_in('Would prune: test1', output)
  921. # must keep the latest non-checkpoint archive:
  922. self.assert_in('Keeping archive: test2', output)
  923. output = self.cmd('list', self.repository_location)
  924. self.assert_in('test1', output)
  925. self.assert_in('test2', output)
  926. self.assert_in('test3.checkpoint', output)
  927. self.assert_in('test3.checkpoint.1', output)
  928. self.cmd('prune', self.repository_location, '--keep-daily=2')
  929. output = self.cmd('list', self.repository_location)
  930. self.assert_not_in('test1', output)
  931. # the latest non-checkpoint archive must be still there:
  932. self.assert_in('test2', output)
  933. def test_prune_repository_save_space(self):
  934. self.cmd('init', self.repository_location)
  935. self.cmd('create', self.repository_location + '::test1', src_dir)
  936. self.cmd('create', self.repository_location + '::test2', src_dir)
  937. output = self.cmd('prune', '-v', '--list', '--dry-run', self.repository_location, '--keep-daily=2')
  938. self.assert_in('Keeping archive: test2', output)
  939. self.assert_in('Would prune: test1', output)
  940. output = self.cmd('list', self.repository_location)
  941. self.assert_in('test1', output)
  942. self.assert_in('test2', output)
  943. self.cmd('prune', '--save-space', self.repository_location, '--keep-daily=2')
  944. output = self.cmd('list', self.repository_location)
  945. self.assert_not_in('test1', output)
  946. self.assert_in('test2', output)
  947. def test_prune_repository_prefix(self):
  948. self.cmd('init', self.repository_location)
  949. self.cmd('create', self.repository_location + '::foo-2015-08-12-10:00', src_dir)
  950. self.cmd('create', self.repository_location + '::foo-2015-08-12-20:00', src_dir)
  951. self.cmd('create', self.repository_location + '::bar-2015-08-12-10:00', src_dir)
  952. self.cmd('create', self.repository_location + '::bar-2015-08-12-20:00', src_dir)
  953. output = self.cmd('prune', '-v', '--list', '--dry-run', self.repository_location, '--keep-daily=2', '--prefix=foo-')
  954. self.assert_in('Keeping archive: foo-2015-08-12-20:00', output)
  955. self.assert_in('Would prune: foo-2015-08-12-10:00', output)
  956. output = self.cmd('list', self.repository_location)
  957. self.assert_in('foo-2015-08-12-10:00', output)
  958. self.assert_in('foo-2015-08-12-20:00', output)
  959. self.assert_in('bar-2015-08-12-10:00', output)
  960. self.assert_in('bar-2015-08-12-20:00', output)
  961. self.cmd('prune', self.repository_location, '--keep-daily=2', '--prefix=foo-')
  962. output = self.cmd('list', self.repository_location)
  963. self.assert_not_in('foo-2015-08-12-10:00', output)
  964. self.assert_in('foo-2015-08-12-20:00', output)
  965. self.assert_in('bar-2015-08-12-10:00', output)
  966. self.assert_in('bar-2015-08-12-20:00', output)
  967. def test_list_prefix(self):
  968. self.cmd('init', self.repository_location)
  969. self.cmd('create', self.repository_location + '::test-1', src_dir)
  970. self.cmd('create', self.repository_location + '::something-else-than-test-1', src_dir)
  971. self.cmd('create', self.repository_location + '::test-2', src_dir)
  972. output = self.cmd('list', '--prefix=test-', self.repository_location)
  973. self.assert_in('test-1', output)
  974. self.assert_in('test-2', output)
  975. self.assert_not_in('something-else', output)
  976. def test_list_list_format(self):
  977. self.cmd('init', self.repository_location)
  978. test_archive = self.repository_location + '::test'
  979. self.cmd('create', test_archive, src_dir)
  980. output_1 = self.cmd('list', test_archive)
  981. output_2 = self.cmd('list', '--list-format', '{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}', test_archive)
  982. output_3 = self.cmd('list', '--list-format', '{mtime:%s} {path}{NEWLINE}', test_archive)
  983. self.assertEqual(output_1, output_2)
  984. self.assertNotEqual(output_1, output_3)
  985. def test_break_lock(self):
  986. self.cmd('init', self.repository_location)
  987. self.cmd('break-lock', self.repository_location)
  988. def test_usage(self):
  989. if self.FORK_DEFAULT:
  990. self.cmd(exit_code=0)
  991. self.cmd('-h', exit_code=0)
  992. else:
  993. self.assert_raises(SystemExit, lambda: self.cmd())
  994. self.assert_raises(SystemExit, lambda: self.cmd('-h'))
  995. def test_help(self):
  996. assert 'Borg' in self.cmd('help')
  997. assert 'patterns' in self.cmd('help', 'patterns')
  998. assert 'Initialize' in self.cmd('help', 'init')
  999. assert 'positional arguments' not in self.cmd('help', 'init', '--epilog-only')
  1000. assert 'This command initializes' not in self.cmd('help', 'init', '--usage-only')
  1001. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  1002. def test_fuse(self):
  1003. def has_noatime(some_file):
  1004. atime_before = os.stat(some_file).st_atime_ns
  1005. try:
  1006. os.close(os.open(some_file, flags_noatime))
  1007. except PermissionError:
  1008. return False
  1009. else:
  1010. atime_after = os.stat(some_file).st_atime_ns
  1011. noatime_used = flags_noatime != flags_normal
  1012. return noatime_used and atime_before == atime_after
  1013. self.cmd('init', self.repository_location)
  1014. self.create_test_files()
  1015. have_noatime = has_noatime('input/file1')
  1016. self.cmd('create', self.repository_location + '::archive', 'input')
  1017. self.cmd('create', self.repository_location + '::archive2', 'input')
  1018. if has_lchflags:
  1019. # remove the file we did not backup, so input and mount become equal
  1020. os.remove(os.path.join('input', 'flagfile'))
  1021. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  1022. # mount the whole repository, archive contents shall show up in archivename subdirs of mountpoint:
  1023. with self.fuse_mount(self.repository_location, mountpoint):
  1024. # bsdflags are not supported by the FUSE mount
  1025. # we also ignore xattrs here, they are tested separately
  1026. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive', 'input'),
  1027. ignore_bsdflags=True, ignore_xattrs=True)
  1028. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive2', 'input'),
  1029. ignore_bsdflags=True, ignore_xattrs=True)
  1030. # mount only 1 archive, its contents shall show up directly in mountpoint:
  1031. with self.fuse_mount(self.repository_location + '::archive', mountpoint):
  1032. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'input'),
  1033. ignore_bsdflags=True, ignore_xattrs=True)
  1034. # regular file
  1035. in_fn = 'input/file1'
  1036. out_fn = os.path.join(mountpoint, 'input', 'file1')
  1037. # stat
  1038. sti1 = os.stat(in_fn)
  1039. sto1 = os.stat(out_fn)
  1040. assert sti1.st_mode == sto1.st_mode
  1041. assert sti1.st_uid == sto1.st_uid
  1042. assert sti1.st_gid == sto1.st_gid
  1043. assert sti1.st_size == sto1.st_size
  1044. if have_noatime:
  1045. assert sti1.st_atime == sto1.st_atime
  1046. assert sti1.st_ctime == sto1.st_ctime
  1047. assert sti1.st_mtime == sto1.st_mtime
  1048. # note: there is another hardlink to this, see below
  1049. assert sti1.st_nlink == sto1.st_nlink == 2
  1050. # read
  1051. with open(in_fn, 'rb') as in_f, open(out_fn, 'rb') as out_f:
  1052. assert in_f.read() == out_f.read()
  1053. # hardlink (to 'input/file1')
  1054. in_fn = 'input/hardlink'
  1055. out_fn = os.path.join(mountpoint, 'input', 'hardlink')
  1056. sti2 = os.stat(in_fn)
  1057. sto2 = os.stat(out_fn)
  1058. assert sti2.st_nlink == sto2.st_nlink == 2
  1059. assert sto1.st_ino == sto2.st_ino
  1060. # symlink
  1061. in_fn = 'input/link1'
  1062. out_fn = os.path.join(mountpoint, 'input', 'link1')
  1063. sti = os.stat(in_fn, follow_symlinks=False)
  1064. sto = os.stat(out_fn, follow_symlinks=False)
  1065. assert stat.S_ISLNK(sti.st_mode)
  1066. assert stat.S_ISLNK(sto.st_mode)
  1067. assert os.readlink(in_fn) == os.readlink(out_fn)
  1068. # FIFO
  1069. out_fn = os.path.join(mountpoint, 'input', 'fifo1')
  1070. sto = os.stat(out_fn)
  1071. assert stat.S_ISFIFO(sto.st_mode)
  1072. # list/read xattrs
  1073. try:
  1074. in_fn = 'input/fusexattr'
  1075. out_fn = os.path.join(mountpoint, 'input', 'fusexattr')
  1076. if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path):
  1077. assert no_selinux(xattr.listxattr(out_fn)) == ['user.foo', ]
  1078. assert xattr.getxattr(out_fn, 'user.foo') == b'bar'
  1079. else:
  1080. assert xattr.listxattr(out_fn) == []
  1081. try:
  1082. xattr.getxattr(out_fn, 'user.foo')
  1083. except OSError as e:
  1084. assert e.errno == llfuse.ENOATTR
  1085. else:
  1086. assert False, "expected OSError(ENOATTR), but no error was raised"
  1087. except OSError as err:
  1088. if sys.platform.startswith(('freebsd', )) and err.errno == errno.ENOTSUP:
  1089. # some systems have no xattr support on FUSE
  1090. pass
  1091. else:
  1092. raise
  1093. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  1094. def test_fuse_allow_damaged_files(self):
  1095. self.cmd('init', self.repository_location)
  1096. self.create_src_archive('archive')
  1097. # Get rid of a chunk and repair it
  1098. archive, repository = self.open_archive('archive')
  1099. with repository:
  1100. for item in archive.iter_items():
  1101. if item[b'path'].endswith('testsuite/archiver.py'):
  1102. repository.delete(item[b'chunks'][-1][0])
  1103. path = item[b'path'] # store full path for later
  1104. break
  1105. else:
  1106. assert False # missed the file
  1107. repository.commit()
  1108. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1109. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  1110. with self.fuse_mount(self.repository_location + '::archive', mountpoint):
  1111. with pytest.raises(OSError) as excinfo:
  1112. open(os.path.join(mountpoint, path))
  1113. assert excinfo.value.errno == errno.EIO
  1114. with self.fuse_mount(self.repository_location + '::archive', mountpoint, '-o', 'allow_damaged_files'):
  1115. open(os.path.join(mountpoint, path)).close()
  1116. def verify_aes_counter_uniqueness(self, method):
  1117. seen = set() # Chunks already seen
  1118. used = set() # counter values already used
  1119. def verify_uniqueness():
  1120. with Repository(self.repository_path) as repository:
  1121. for id, _ in repository.open_index(repository.get_transaction_id()).iteritems():
  1122. data = repository.get(id)
  1123. hash = sha256(data).digest()
  1124. if hash not in seen:
  1125. seen.add(hash)
  1126. num_blocks = num_aes_blocks(len(data) - 41)
  1127. nonce = bytes_to_long(data[33:41])
  1128. for counter in range(nonce, nonce + num_blocks):
  1129. self.assert_not_in(counter, used)
  1130. used.add(counter)
  1131. self.create_test_files()
  1132. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  1133. self.cmd('init', '--encryption=' + method, self.repository_location)
  1134. verify_uniqueness()
  1135. self.cmd('create', self.repository_location + '::test', 'input')
  1136. verify_uniqueness()
  1137. self.cmd('create', self.repository_location + '::test.2', 'input')
  1138. verify_uniqueness()
  1139. self.cmd('delete', self.repository_location + '::test.2')
  1140. verify_uniqueness()
  1141. self.assert_equal(used, set(range(len(used))))
  1142. def test_aes_counter_uniqueness_keyfile(self):
  1143. self.verify_aes_counter_uniqueness('keyfile')
  1144. def test_aes_counter_uniqueness_passphrase(self):
  1145. self.verify_aes_counter_uniqueness('repokey')
  1146. def test_debug_dump_archive_items(self):
  1147. self.create_test_files()
  1148. self.cmd('init', self.repository_location)
  1149. self.cmd('create', self.repository_location + '::test', 'input')
  1150. with changedir('output'):
  1151. output = self.cmd('debug-dump-archive-items', self.repository_location + '::test')
  1152. output_dir = sorted(os.listdir('output'))
  1153. assert len(output_dir) > 0 and output_dir[0].startswith('000000_')
  1154. assert 'Done.' in output
  1155. def test_debug_dump_repo_objs(self):
  1156. self.create_test_files()
  1157. self.cmd('init', self.repository_location)
  1158. self.cmd('create', self.repository_location + '::test', 'input')
  1159. with changedir('output'):
  1160. output = self.cmd('debug-dump-repo-objs', self.repository_location)
  1161. output_dir = sorted(os.listdir('output'))
  1162. assert len(output_dir) > 0 and output_dir[0].startswith('000000_')
  1163. assert 'Done.' in output
  1164. def test_debug_put_get_delete_obj(self):
  1165. self.cmd('init', self.repository_location)
  1166. data = b'some data'
  1167. hexkey = sha256(data).hexdigest()
  1168. self.create_regular_file('file', contents=data)
  1169. output = self.cmd('debug-put-obj', self.repository_location, 'input/file')
  1170. assert hexkey in output
  1171. output = self.cmd('debug-get-obj', self.repository_location, hexkey, 'output/file')
  1172. assert hexkey in output
  1173. with open('output/file', 'rb') as f:
  1174. data_read = f.read()
  1175. assert data == data_read
  1176. output = self.cmd('debug-delete-obj', self.repository_location, hexkey)
  1177. assert "deleted" in output
  1178. output = self.cmd('debug-delete-obj', self.repository_location, hexkey)
  1179. assert "not found" in output
  1180. output = self.cmd('debug-delete-obj', self.repository_location, 'invalid')
  1181. assert "is invalid" in output
  1182. def test_key_export_keyfile(self):
  1183. export_file = self.output_path + '/exported'
  1184. self.cmd('init', self.repository_location, '--encryption', 'keyfile')
  1185. repo_id = self._extract_repository_id(self.repository_path)
  1186. self.cmd('key', 'export', self.repository_location, export_file)
  1187. with open(export_file, 'r') as fd:
  1188. export_contents = fd.read()
  1189. assert export_contents.startswith('BORG_KEY ' + bin_to_hex(repo_id) + '\n')
  1190. key_file = self.keys_path + '/' + os.listdir(self.keys_path)[0]
  1191. with open(key_file, 'r') as fd:
  1192. key_contents = fd.read()
  1193. assert key_contents == export_contents
  1194. os.unlink(key_file)
  1195. self.cmd('key', 'import', self.repository_location, export_file)
  1196. with open(key_file, 'r') as fd:
  1197. key_contents2 = fd.read()
  1198. assert key_contents2 == key_contents
  1199. def test_key_export_repokey(self):
  1200. export_file = self.output_path + '/exported'
  1201. self.cmd('init', self.repository_location, '--encryption', 'repokey')
  1202. repo_id = self._extract_repository_id(self.repository_path)
  1203. self.cmd('key', 'export', self.repository_location, export_file)
  1204. with open(export_file, 'r') as fd:
  1205. export_contents = fd.read()
  1206. assert export_contents.startswith('BORG_KEY ' + bin_to_hex(repo_id) + '\n')
  1207. with Repository(self.repository_path) as repository:
  1208. repo_key = RepoKey(repository)
  1209. repo_key.load(None, Passphrase.env_passphrase())
  1210. backup_key = KeyfileKey(key.KeyTestCase.MockRepository())
  1211. backup_key.load(export_file, Passphrase.env_passphrase())
  1212. assert repo_key.enc_key == backup_key.enc_key
  1213. with Repository(self.repository_path) as repository:
  1214. repository.save_key(b'')
  1215. self.cmd('key', 'import', self.repository_location, export_file)
  1216. with Repository(self.repository_path) as repository:
  1217. repo_key2 = RepoKey(repository)
  1218. repo_key2.load(None, Passphrase.env_passphrase())
  1219. assert repo_key2.enc_key == repo_key2.enc_key
  1220. def test_key_export_qr(self):
  1221. export_file = self.output_path + '/exported.html'
  1222. self.cmd('init', self.repository_location, '--encryption', 'repokey')
  1223. repo_id = self._extract_repository_id(self.repository_path)
  1224. self.cmd('key', 'export', '--qr-html', self.repository_location, export_file)
  1225. with open(export_file, 'r', encoding='utf-8') as fd:
  1226. export_contents = fd.read()
  1227. assert bin_to_hex(repo_id) in export_contents
  1228. assert export_contents.startswith('<!doctype html>')
  1229. assert export_contents.endswith('</html>')
  1230. def test_key_import_errors(self):
  1231. export_file = self.output_path + '/exported'
  1232. self.cmd('init', self.repository_location, '--encryption', 'keyfile')
  1233. self.cmd('key', 'import', self.repository_location, export_file, exit_code=EXIT_ERROR)
  1234. with open(export_file, 'w') as fd:
  1235. fd.write('something not a key\n')
  1236. if self.FORK_DEFAULT:
  1237. self.cmd('key', 'import', self.repository_location, export_file, exit_code=2)
  1238. else:
  1239. self.assert_raises(NotABorgKeyFile, lambda: self.cmd('key', 'import', self.repository_location, export_file))
  1240. with open(export_file, 'w') as fd:
  1241. fd.write('BORG_KEY a0a0a0\n')
  1242. if self.FORK_DEFAULT:
  1243. self.cmd('key', 'import', self.repository_location, export_file, exit_code=2)
  1244. else:
  1245. self.assert_raises(RepoIdMismatch, lambda: self.cmd('key', 'import', self.repository_location, export_file))
  1246. def test_key_export_paperkey(self):
  1247. repo_id = 'e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239'
  1248. export_file = self.output_path + '/exported'
  1249. self.cmd('init', self.repository_location, '--encryption', 'keyfile')
  1250. self._set_repository_id(self.repository_path, unhexlify(repo_id))
  1251. key_file = self.keys_path + '/' + os.listdir(self.keys_path)[0]
  1252. with open(key_file, 'w') as fd:
  1253. fd.write(KeyfileKey.FILE_ID + ' ' + repo_id + '\n')
  1254. fd.write(b2a_base64(b'abcdefghijklmnopqrstu').decode())
  1255. self.cmd('key', 'export', '--paper', self.repository_location, export_file)
  1256. with open(export_file, 'r') as fd:
  1257. export_contents = fd.read()
  1258. assert export_contents == """To restore key use borg key import --paper /path/to/repo
  1259. BORG PAPER KEY v1
  1260. id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02
  1261. 1: 616263 646566 676869 6a6b6c 6d6e6f 707172 - 6d
  1262. 2: 737475 - 88
  1263. """
  1264. @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
  1265. class ArchiverTestCaseBinary(ArchiverTestCase):
  1266. EXE = 'borg.exe'
  1267. FORK_DEFAULT = True
  1268. @unittest.skip('test_basic_functionality seems incompatible with fakeroot and/or the binary.')
  1269. def test_basic_functionality(self):
  1270. pass
  1271. @unittest.skip('test_overwrite seems incompatible with fakeroot and/or the binary.')
  1272. def test_overwrite(self):
  1273. pass
  1274. def test_fuse(self):
  1275. if fakeroot_detected():
  1276. unittest.skip('test_fuse with the binary is not compatible with fakeroot')
  1277. else:
  1278. super().test_fuse()
  1279. class ArchiverCheckTestCase(ArchiverTestCaseBase):
  1280. def setUp(self):
  1281. super().setUp()
  1282. with patch.object(ChunkBuffer, 'BUFFER_SIZE', 10):
  1283. self.cmd('init', self.repository_location)
  1284. self.create_src_archive('archive1')
  1285. self.create_src_archive('archive2')
  1286. def test_check_usage(self):
  1287. output = self.cmd('check', '-v', self.repository_location, exit_code=0)
  1288. self.assert_in('Starting repository check', output)
  1289. self.assert_in('Starting archive consistency check', output)
  1290. output = self.cmd('check', '-v', '--repository-only', self.repository_location, exit_code=0)
  1291. self.assert_in('Starting repository check', output)
  1292. self.assert_not_in('Starting archive consistency check', output)
  1293. output = self.cmd('check', '-v', '--archives-only', self.repository_location, exit_code=0)
  1294. self.assert_not_in('Starting repository check', output)
  1295. self.assert_in('Starting archive consistency check', output)
  1296. output = self.cmd('check', '-v', '--archives-only', '--prefix=archive2', self.repository_location, exit_code=0)
  1297. self.assert_not_in('archive1', output)
  1298. def test_missing_file_chunk(self):
  1299. archive, repository = self.open_archive('archive1')
  1300. with repository:
  1301. for item in archive.iter_items():
  1302. if item[b'path'].endswith('testsuite/archiver.py'):
  1303. valid_chunks = item[b'chunks']
  1304. killed_chunk = valid_chunks[-1]
  1305. repository.delete(killed_chunk[0])
  1306. break
  1307. else:
  1308. self.assert_true(False) # should not happen
  1309. repository.commit()
  1310. self.cmd('check', self.repository_location, exit_code=1)
  1311. output = self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1312. self.assert_in('New missing file chunk detected', output)
  1313. self.cmd('check', self.repository_location, exit_code=0)
  1314. # check that the file in the old archives has now a different chunk list without the killed chunk
  1315. for archive_name in ('archive1', 'archive2'):
  1316. archive, repository = self.open_archive(archive_name)
  1317. with repository:
  1318. for item in archive.iter_items():
  1319. if item[b'path'].endswith('testsuite/archiver.py'):
  1320. self.assert_not_equal(valid_chunks, item[b'chunks'])
  1321. self.assert_not_in(killed_chunk, item[b'chunks'])
  1322. break
  1323. else:
  1324. self.assert_true(False) # should not happen
  1325. # do a fresh backup (that will include the killed chunk)
  1326. with patch.object(ChunkBuffer, 'BUFFER_SIZE', 10):
  1327. self.create_src_archive('archive3')
  1328. # check should be able to heal the file now:
  1329. output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0)
  1330. self.assert_in('Healed previously missing file chunk', output)
  1331. self.assert_in('testsuite/archiver.py: Completely healed previously damaged file!', output)
  1332. # check that the file in the old archives has the correct chunks again
  1333. for archive_name in ('archive1', 'archive2'):
  1334. archive, repository = self.open_archive(archive_name)
  1335. with repository:
  1336. for item in archive.iter_items():
  1337. if item[b'path'].endswith('testsuite/archiver.py'):
  1338. self.assert_equal(valid_chunks, item[b'chunks'])
  1339. break
  1340. else:
  1341. self.assert_true(False) # should not happen
  1342. def test_missing_archive_item_chunk(self):
  1343. archive, repository = self.open_archive('archive1')
  1344. with repository:
  1345. repository.delete(archive.metadata[b'items'][-5])
  1346. repository.commit()
  1347. self.cmd('check', self.repository_location, exit_code=1)
  1348. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1349. self.cmd('check', self.repository_location, exit_code=0)
  1350. def test_missing_archive_metadata(self):
  1351. archive, repository = self.open_archive('archive1')
  1352. with repository:
  1353. repository.delete(archive.id)
  1354. repository.commit()
  1355. self.cmd('check', self.repository_location, exit_code=1)
  1356. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1357. self.cmd('check', self.repository_location, exit_code=0)
  1358. def test_missing_manifest(self):
  1359. archive, repository = self.open_archive('archive1')
  1360. with repository:
  1361. repository.delete(Manifest.MANIFEST_ID)
  1362. repository.commit()
  1363. self.cmd('check', self.repository_location, exit_code=1)
  1364. output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0)
  1365. self.assert_in('archive1', output)
  1366. self.assert_in('archive2', output)
  1367. self.cmd('check', self.repository_location, exit_code=0)
  1368. def test_corrupted_manifest(self):
  1369. archive, repository = self.open_archive('archive1')
  1370. with repository:
  1371. manifest = repository.get(Manifest.MANIFEST_ID)
  1372. corrupted_manifest = manifest + b'corrupted!'
  1373. repository.put(Manifest.MANIFEST_ID, corrupted_manifest)
  1374. repository.commit()
  1375. self.cmd('check', self.repository_location, exit_code=1)
  1376. output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0)
  1377. self.assert_in('archive1', output)
  1378. self.assert_in('archive2', output)
  1379. self.cmd('check', self.repository_location, exit_code=0)
  1380. def test_manifest_rebuild_corrupted_chunk(self):
  1381. archive, repository = self.open_archive('archive1')
  1382. with repository:
  1383. manifest = repository.get(Manifest.MANIFEST_ID)
  1384. corrupted_manifest = manifest + b'corrupted!'
  1385. repository.put(Manifest.MANIFEST_ID, corrupted_manifest)
  1386. chunk = repository.get(archive.id)
  1387. corrupted_chunk = chunk + b'corrupted!'
  1388. repository.put(archive.id, corrupted_chunk)
  1389. repository.commit()
  1390. self.cmd('check', self.repository_location, exit_code=1)
  1391. output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0)
  1392. self.assert_in('archive2', output)
  1393. self.cmd('check', self.repository_location, exit_code=0)
  1394. def test_manifest_rebuild_duplicate_archive(self):
  1395. archive, repository = self.open_archive('archive1')
  1396. key = archive.key
  1397. with repository:
  1398. manifest = repository.get(Manifest.MANIFEST_ID)
  1399. corrupted_manifest = manifest + b'corrupted!'
  1400. repository.put(Manifest.MANIFEST_ID, corrupted_manifest)
  1401. archive = msgpack.packb({
  1402. 'cmdline': [],
  1403. 'items': [],
  1404. 'hostname': 'foo',
  1405. 'username': 'bar',
  1406. 'name': 'archive1',
  1407. 'time': '2016-12-15T18:49:51.849711',
  1408. 'version': 1,
  1409. })
  1410. archive_id = key.id_hash(archive)
  1411. repository.put(archive_id, key.encrypt(archive))
  1412. repository.commit()
  1413. self.cmd('check', self.repository_location, exit_code=1)
  1414. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1415. output = self.cmd('list', self.repository_location)
  1416. self.assert_in('archive1', output)
  1417. self.assert_in('archive1.1', output)
  1418. self.assert_in('archive2', output)
  1419. def test_extra_chunks(self):
  1420. self.cmd('check', self.repository_location, exit_code=0)
  1421. with Repository(self.repository_location, exclusive=True) as repository:
  1422. repository.put(b'01234567890123456789012345678901', b'xxxx')
  1423. repository.commit()
  1424. self.cmd('check', self.repository_location, exit_code=1)
  1425. self.cmd('check', self.repository_location, exit_code=1)
  1426. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1427. self.cmd('check', self.repository_location, exit_code=0)
  1428. self.cmd('extract', '--dry-run', self.repository_location + '::archive1', exit_code=0)
  1429. def test_empty_repository(self):
  1430. with Repository(self.repository_location, exclusive=True) as repository:
  1431. for id_ in repository.list():
  1432. repository.delete(id_)
  1433. repository.commit()
  1434. self.cmd('check', self.repository_location, exit_code=1)
  1435. def test_attic013_acl_bug(self):
  1436. # Attic up to release 0.13 contained a bug where every item unintentionally received
  1437. # a b'acl'=None key-value pair.
  1438. # This bug can still live on in Borg repositories (through borg upgrade).
  1439. archive, repository = self.open_archive('archive1')
  1440. with repository:
  1441. manifest, key = Manifest.load(repository)
  1442. with Cache(repository, key, manifest) as cache:
  1443. archive = Archive(repository, key, manifest, '0.13', cache=cache, create=True)
  1444. archive.items_buffer.add({
  1445. # path and mtime are required.
  1446. b'path': '1234',
  1447. b'mtime': 0,
  1448. # acl is the offending key.
  1449. b'acl': None
  1450. })
  1451. archive.save()
  1452. self.cmd('check', self.repository_location, exit_code=0)
  1453. class ManifestAuthenticationTest(ArchiverTestCaseBase):
  1454. def spoof_manifest(self, repository):
  1455. with repository:
  1456. _, key = Manifest.load(repository)
  1457. repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb({
  1458. 'version': 1,
  1459. 'archives': {},
  1460. 'config': {},
  1461. 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(),
  1462. })))
  1463. repository.commit()
  1464. def test_fresh_init_tam_required(self):
  1465. self.cmd('init', self.repository_location)
  1466. repository = Repository(self.repository_path, exclusive=True)
  1467. with repository:
  1468. manifest, key = Manifest.load(repository)
  1469. repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb({
  1470. 'version': 1,
  1471. 'archives': {},
  1472. 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(),
  1473. })))
  1474. repository.commit()
  1475. with pytest.raises(TAMRequiredError):
  1476. self.cmd('list', self.repository_location)
  1477. def test_not_required(self):
  1478. self.cmd('init', self.repository_location)
  1479. self.create_src_archive('archive1234')
  1480. repository = Repository(self.repository_path, exclusive=True)
  1481. with repository:
  1482. shutil.rmtree(get_security_dir(bin_to_hex(repository.id)))
  1483. _, key = Manifest.load(repository)
  1484. key.tam_required = False
  1485. key.change_passphrase(key._passphrase)
  1486. manifest = msgpack.unpackb(key.decrypt(None, repository.get(Manifest.MANIFEST_ID)))
  1487. del manifest[b'tam']
  1488. repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb(manifest)))
  1489. repository.commit()
  1490. output = self.cmd('list', '--debug', self.repository_location)
  1491. assert 'archive1234' in output
  1492. assert 'TAM not found and not required' in output
  1493. # Run upgrade
  1494. self.cmd('upgrade', '--tam', self.repository_location)
  1495. # Manifest must be authenticated now
  1496. output = self.cmd('list', '--debug', self.repository_location)
  1497. assert 'archive1234' in output
  1498. assert 'TAM-verified manifest' in output
  1499. # Try to spoof / modify pre-1.0.9
  1500. self.spoof_manifest(repository)
  1501. # Fails
  1502. with pytest.raises(TAMRequiredError):
  1503. self.cmd('list', self.repository_location)
  1504. # Force upgrade
  1505. self.cmd('upgrade', '--tam', '--force', self.repository_location)
  1506. self.cmd('list', self.repository_location)
  1507. def test_disable(self):
  1508. self.cmd('init', self.repository_location)
  1509. self.create_src_archive('archive1234')
  1510. self.cmd('upgrade', '--disable-tam', self.repository_location)
  1511. repository = Repository(self.repository_path, exclusive=True)
  1512. self.spoof_manifest(repository)
  1513. assert not self.cmd('list', self.repository_location)
  1514. def test_disable2(self):
  1515. self.cmd('init', self.repository_location)
  1516. self.create_src_archive('archive1234')
  1517. repository = Repository(self.repository_path, exclusive=True)
  1518. self.spoof_manifest(repository)
  1519. self.cmd('upgrade', '--disable-tam', self.repository_location)
  1520. assert not self.cmd('list', self.repository_location)
  1521. @pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs')
  1522. class RemoteArchiverTestCase(ArchiverTestCase):
  1523. prefix = '__testsuite__:'
  1524. def test_remote_repo_restrict_to_path(self):
  1525. # restricted to repo directory itself:
  1526. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', self.repository_path]):
  1527. self.cmd('init', self.repository_location)
  1528. # restricted to repo directory itself, fail for other directories with same prefix:
  1529. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', self.repository_path]):
  1530. self.assert_raises(PathNotAllowed, lambda: self.cmd('init', self.repository_location + '_0'))
  1531. # restricted to a completely different path:
  1532. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo']):
  1533. self.assert_raises(PathNotAllowed, lambda: self.cmd('init', self.repository_location + '_1'))
  1534. path_prefix = os.path.dirname(self.repository_path)
  1535. # restrict to repo directory's parent directory:
  1536. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', path_prefix]):
  1537. self.cmd('init', self.repository_location + '_2')
  1538. # restrict to repo directory's parent directory and another directory:
  1539. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]):
  1540. self.cmd('init', self.repository_location + '_3')
  1541. @unittest.skip('only works locally')
  1542. def test_debug_put_get_delete_obj(self):
  1543. pass
  1544. def test_strip_components_doesnt_leak(self):
  1545. self.cmd('init', self.repository_location)
  1546. self.create_regular_file('dir/file', contents=b"test file contents 1")
  1547. self.create_regular_file('dir/file2', contents=b"test file contents 2")
  1548. self.create_regular_file('skipped-file1', contents=b"test file contents 3")
  1549. self.create_regular_file('skipped-file2', contents=b"test file contents 4")
  1550. self.create_regular_file('skipped-file3', contents=b"test file contents 5")
  1551. self.cmd('create', self.repository_location + '::test', 'input')
  1552. marker = 'cached responses left in RemoteRepository'
  1553. with changedir('output'):
  1554. res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '3')
  1555. self.assert_true(marker not in res)
  1556. with self.assert_creates_file('file'):
  1557. res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '2')
  1558. self.assert_true(marker not in res)
  1559. with self.assert_creates_file('dir/file'):
  1560. res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '1')
  1561. self.assert_true(marker not in res)
  1562. with self.assert_creates_file('input/dir/file'):
  1563. res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '0')
  1564. self.assert_true(marker not in res)
  1565. def test_get_args():
  1566. archiver = Archiver()
  1567. # everything normal:
  1568. # first param is argv as produced by ssh forced command,
  1569. # second param is like from SSH_ORIGINAL_COMMAND env variable
  1570. args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
  1571. 'borg serve --info --umask=0027')
  1572. assert args.func == archiver.do_serve
  1573. assert args.restrict_to_paths == ['/p1', '/p2']
  1574. assert args.umask == 0o027
  1575. assert args.log_level == 'info'
  1576. # trying to cheat - break out of path restriction
  1577. args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
  1578. 'borg serve --restrict-to-path=/')
  1579. assert args.restrict_to_paths == ['/p1', '/p2']
  1580. # trying to cheat - try to execute different subcommand
  1581. args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
  1582. 'borg init /')
  1583. assert args.func == archiver.do_serve
  1584. class TestBuildFilter:
  1585. def test_basic(self):
  1586. matcher = PatternMatcher()
  1587. matcher.add([parse_pattern('included')], True)
  1588. filter = Archiver.build_filter(matcher)
  1589. assert filter({b'path': 'included'})
  1590. assert filter({b'path': 'included/file'})
  1591. assert not filter({b'path': 'something else'})
  1592. def test_empty(self):
  1593. matcher = PatternMatcher(fallback=True)
  1594. filter = Archiver.build_filter(matcher)
  1595. assert filter({b'path': 'anything'})
  1596. def test_strip_components(self):
  1597. matcher = PatternMatcher(fallback=True)
  1598. filter = Archiver.build_filter(matcher, strip_components=1)
  1599. assert not filter({b'path': 'shallow'})
  1600. assert not filter({b'path': 'shallow/'}) # can this even happen? paths are normalized...
  1601. assert filter({b'path': 'deep enough/file'})
  1602. assert filter({b'path': 'something/dir/file'})