archiver.py 92 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942
  1. from configparser import ConfigParser
  2. import errno
  3. import os
  4. import inspect
  5. from io import StringIO
  6. import logging
  7. import random
  8. import socket
  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 pytest
  19. from .. import xattr, helpers, platform
  20. from ..archive import Archive, ChunkBuffer, ArchiveRecreater
  21. from ..archiver import Archiver
  22. from ..cache import Cache
  23. from ..constants import * # NOQA
  24. from ..crypto import bytes_to_long, num_aes_blocks
  25. from ..helpers import Chunk, Manifest, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, bin_to_hex
  26. from ..key import KeyfileKeyBase
  27. from ..remote import RemoteRepository, PathNotAllowed
  28. from ..repository import Repository
  29. from . import has_lchflags, has_llfuse
  30. from . import BaseTestCase, changedir, environment_variable
  31. try:
  32. import llfuse
  33. except ImportError:
  34. pass
  35. src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
  36. def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw):
  37. if fork:
  38. try:
  39. if exe is None:
  40. borg = (sys.executable, '-m', 'borg.archiver')
  41. elif isinstance(exe, str):
  42. borg = (exe, )
  43. elif not isinstance(exe, tuple):
  44. raise ValueError('exe must be None, a tuple or a str')
  45. output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT)
  46. ret = 0
  47. except subprocess.CalledProcessError as e:
  48. output = e.output
  49. ret = e.returncode
  50. return ret, os.fsdecode(output)
  51. else:
  52. stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr
  53. try:
  54. sys.stdin = StringIO()
  55. sys.stdout = sys.stderr = output = StringIO()
  56. if archiver is None:
  57. archiver = Archiver()
  58. archiver.prerun_checks = lambda *args: None
  59. archiver.exit_code = EXIT_SUCCESS
  60. args = archiver.parse_args(list(args))
  61. ret = archiver.run(args)
  62. return ret, output.getvalue()
  63. finally:
  64. sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr
  65. # check if the binary "borg.exe" is available
  66. try:
  67. exec_cmd('help', exe='borg.exe', fork=True)
  68. BORG_EXES = ['python', 'binary', ]
  69. except FileNotFoundError:
  70. BORG_EXES = ['python', ]
  71. @pytest.fixture(params=BORG_EXES)
  72. def cmd(request):
  73. if request.param == 'python':
  74. exe = None
  75. elif request.param == 'binary':
  76. exe = 'borg.exe'
  77. else:
  78. raise ValueError("param must be 'python' or 'binary'")
  79. def exec_fn(*args, **kw):
  80. return exec_cmd(*args, exe=exe, fork=True, **kw)
  81. return exec_fn
  82. def test_return_codes(cmd, tmpdir):
  83. repo = tmpdir.mkdir('repo')
  84. input = tmpdir.mkdir('input')
  85. output = tmpdir.mkdir('output')
  86. input.join('test_file').write('content')
  87. rc, out = cmd('init', '--encryption=none', '%s' % str(repo))
  88. assert rc == EXIT_SUCCESS
  89. rc, out = cmd('create', '%s::archive' % repo, str(input))
  90. assert rc == EXIT_SUCCESS
  91. with changedir(str(output)):
  92. rc, out = cmd('extract', '%s::archive' % repo)
  93. assert rc == EXIT_SUCCESS
  94. rc, out = cmd('extract', '%s::archive' % repo, 'does/not/match')
  95. assert rc == EXIT_WARNING # pattern did not match
  96. rc, out = cmd('create', '%s::archive' % repo, str(input))
  97. assert rc == EXIT_ERROR # duplicate archive name
  98. """
  99. test_disk_full is very slow and not recommended to be included in daily testing.
  100. for this test, an empty, writable 16MB filesystem mounted on DF_MOUNT is required.
  101. for speed and other reasons, it is recommended that the underlying block device is
  102. in RAM, not a magnetic or flash disk.
  103. assuming /tmp is a tmpfs (in memory filesystem), one can use this:
  104. dd if=/dev/zero of=/tmp/borg-disk bs=16M count=1
  105. mkfs.ext4 /tmp/borg-disk
  106. mkdir /tmp/borg-mount
  107. sudo mount /tmp/borg-disk /tmp/borg-mount
  108. if the directory does not exist, the test will be skipped.
  109. """
  110. DF_MOUNT = '/tmp/borg-mount'
  111. @pytest.mark.skipif(not os.path.exists(DF_MOUNT), reason="needs a 16MB fs mounted on %s" % DF_MOUNT)
  112. def test_disk_full(cmd):
  113. def make_files(dir, count, size, rnd=True):
  114. shutil.rmtree(dir, ignore_errors=True)
  115. os.mkdir(dir)
  116. if rnd:
  117. count = random.randint(1, count)
  118. if size > 1:
  119. size = random.randint(1, size)
  120. for i in range(count):
  121. fn = os.path.join(dir, "file%03d" % i)
  122. with open(fn, 'wb') as f:
  123. data = os.urandom(size)
  124. f.write(data)
  125. with environment_variable(BORG_CHECK_I_KNOW_WHAT_I_AM_DOING='YES'):
  126. mount = DF_MOUNT
  127. assert os.path.exists(mount)
  128. repo = os.path.join(mount, 'repo')
  129. input = os.path.join(mount, 'input')
  130. reserve = os.path.join(mount, 'reserve')
  131. for j in range(100):
  132. shutil.rmtree(repo, ignore_errors=True)
  133. shutil.rmtree(input, ignore_errors=True)
  134. # keep some space and some inodes in reserve that we can free up later:
  135. make_files(reserve, 80, 100000, rnd=False)
  136. rc, out = cmd('init', repo)
  137. if rc != EXIT_SUCCESS:
  138. print('init', rc, out)
  139. assert rc == EXIT_SUCCESS
  140. try:
  141. success, i = True, 0
  142. while success:
  143. i += 1
  144. try:
  145. make_files(input, 20, 200000)
  146. except OSError as err:
  147. if err.errno == errno.ENOSPC:
  148. # already out of space
  149. break
  150. raise
  151. try:
  152. rc, out = cmd('create', '%s::test%03d' % (repo, i), input)
  153. success = rc == EXIT_SUCCESS
  154. if not success:
  155. print('create', rc, out)
  156. finally:
  157. # make sure repo is not locked
  158. shutil.rmtree(os.path.join(repo, 'lock.exclusive'), ignore_errors=True)
  159. os.remove(os.path.join(repo, 'lock.roster'))
  160. finally:
  161. # now some error happened, likely we are out of disk space.
  162. # free some space so we can expect borg to be able to work normally:
  163. shutil.rmtree(reserve, ignore_errors=True)
  164. rc, out = cmd('list', repo)
  165. if rc != EXIT_SUCCESS:
  166. print('list', rc, out)
  167. rc, out = cmd('check', '--repair', repo)
  168. if rc != EXIT_SUCCESS:
  169. print('check', rc, out)
  170. assert rc == EXIT_SUCCESS
  171. class ArchiverTestCaseBase(BaseTestCase):
  172. EXE = None # python source based
  173. FORK_DEFAULT = False
  174. prefix = ''
  175. def setUp(self):
  176. os.environ['BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
  177. os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
  178. os.environ['BORG_RECREATE_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
  179. os.environ['BORG_PASSPHRASE'] = 'waytooeasyonlyfortests'
  180. self.archiver = not self.FORK_DEFAULT and Archiver() or None
  181. self.tmpdir = tempfile.mkdtemp()
  182. self.repository_path = os.path.join(self.tmpdir, 'repository')
  183. self.repository_location = self.prefix + self.repository_path
  184. self.input_path = os.path.join(self.tmpdir, 'input')
  185. self.output_path = os.path.join(self.tmpdir, 'output')
  186. self.keys_path = os.path.join(self.tmpdir, 'keys')
  187. self.cache_path = os.path.join(self.tmpdir, 'cache')
  188. self.exclude_file_path = os.path.join(self.tmpdir, 'excludes')
  189. os.environ['BORG_KEYS_DIR'] = self.keys_path
  190. os.environ['BORG_CACHE_DIR'] = self.cache_path
  191. os.mkdir(self.input_path)
  192. os.mkdir(self.output_path)
  193. os.mkdir(self.keys_path)
  194. os.mkdir(self.cache_path)
  195. with open(self.exclude_file_path, 'wb') as fd:
  196. fd.write(b'input/file2\n# A comment line, then a blank line\n\n')
  197. self._old_wd = os.getcwd()
  198. os.chdir(self.tmpdir)
  199. def tearDown(self):
  200. os.chdir(self._old_wd)
  201. shutil.rmtree(self.tmpdir)
  202. def cmd(self, *args, **kw):
  203. exit_code = kw.pop('exit_code', 0)
  204. fork = kw.pop('fork', None)
  205. if fork is None:
  206. fork = self.FORK_DEFAULT
  207. ret, output = exec_cmd(*args, fork=fork, exe=self.EXE, archiver=self.archiver, **kw)
  208. if ret != exit_code:
  209. print(output)
  210. self.assert_equal(ret, exit_code)
  211. return output
  212. def create_src_archive(self, name):
  213. self.cmd('create', self.repository_location + '::' + name, src_dir)
  214. def create_regular_file(self, name, size=0, contents=None):
  215. filename = os.path.join(self.input_path, name)
  216. if not os.path.exists(os.path.dirname(filename)):
  217. os.makedirs(os.path.dirname(filename))
  218. with open(filename, 'wb') as fd:
  219. if contents is None:
  220. contents = b'X' * size
  221. fd.write(contents)
  222. def create_test_files(self):
  223. """Create a minimal test case including all supported file types
  224. """
  225. # File
  226. self.create_regular_file('empty', size=0)
  227. # next code line raises OverflowError on 32bit cpu (raspberry pi 2):
  228. # 2600-01-01 > 2**64 ns
  229. # os.utime('input/empty', (19880895600, 19880895600))
  230. # thus, we better test with something not that far in future:
  231. # 2038-01-19 (1970 + 2^31 - 1 seconds) is the 32bit "deadline":
  232. os.utime('input/empty', (2**31 - 1, 2**31 - 1))
  233. self.create_regular_file('file1', size=1024 * 80)
  234. self.create_regular_file('flagfile', size=1024)
  235. # Directory
  236. self.create_regular_file('dir2/file2', size=1024 * 80)
  237. # File mode
  238. os.chmod('input/file1', 0o4755)
  239. # Hard link
  240. os.link(os.path.join(self.input_path, 'file1'),
  241. os.path.join(self.input_path, 'hardlink'))
  242. # Symlink
  243. os.symlink('somewhere', os.path.join(self.input_path, 'link1'))
  244. if xattr.is_enabled(self.input_path):
  245. xattr.setxattr(os.path.join(self.input_path, 'file1'), 'user.foo', b'bar')
  246. # XXX this always fails for me
  247. # ubuntu 14.04, on a TMP dir filesystem with user_xattr, using fakeroot
  248. # same for newer ubuntu and centos.
  249. # if this is supported just on specific platform, platform should be checked first,
  250. # so that the test setup for all tests using it does not fail here always for others.
  251. # xattr.setxattr(os.path.join(self.input_path, 'link1'), 'user.foo_symlink', b'bar_symlink', follow_symlinks=False)
  252. # FIFO node
  253. os.mkfifo(os.path.join(self.input_path, 'fifo1'))
  254. if has_lchflags:
  255. platform.set_flags(os.path.join(self.input_path, 'flagfile'), stat.UF_NODUMP)
  256. try:
  257. # Block device
  258. os.mknod('input/bdev', 0o600 | stat.S_IFBLK, os.makedev(10, 20))
  259. # Char device
  260. os.mknod('input/cdev', 0o600 | stat.S_IFCHR, os.makedev(30, 40))
  261. # File mode
  262. os.chmod('input/dir2', 0o555) # if we take away write perms, we need root to remove contents
  263. # File owner
  264. os.chown('input/file1', 100, 200)
  265. have_root = True # we have (fake)root
  266. except PermissionError:
  267. have_root = False
  268. return have_root
  269. class ArchiverTestCase(ArchiverTestCaseBase):
  270. def test_basic_functionality(self):
  271. have_root = self.create_test_files()
  272. # fork required to test show-rc output
  273. output = self.cmd('init', '--show-version', '--show-rc', self.repository_location, fork=True)
  274. self.assert_in('borgbackup version', output)
  275. self.assert_in('terminating with success status, rc 0', output)
  276. self.cmd('create', self.repository_location + '::test', 'input')
  277. output = self.cmd('create', '--stats', self.repository_location + '::test.2', 'input')
  278. self.assert_in('Archive name: test.2', output)
  279. self.assert_in('This archive: ', output)
  280. with changedir('output'):
  281. self.cmd('extract', self.repository_location + '::test')
  282. list_output = self.cmd('list', '--short', self.repository_location)
  283. self.assert_in('test', list_output)
  284. self.assert_in('test.2', list_output)
  285. expected = [
  286. 'input',
  287. 'input/bdev',
  288. 'input/cdev',
  289. 'input/dir2',
  290. 'input/dir2/file2',
  291. 'input/empty',
  292. 'input/fifo1',
  293. 'input/file1',
  294. 'input/flagfile',
  295. 'input/hardlink',
  296. 'input/link1',
  297. ]
  298. if not have_root:
  299. # we could not create these device files without (fake)root
  300. expected.remove('input/bdev')
  301. expected.remove('input/cdev')
  302. if has_lchflags:
  303. # remove the file we did not backup, so input and output become equal
  304. expected.remove('input/flagfile') # this file is UF_NODUMP
  305. os.remove(os.path.join('input', 'flagfile'))
  306. list_output = self.cmd('list', '--short', self.repository_location + '::test')
  307. for name in expected:
  308. self.assert_in(name, list_output)
  309. self.assert_dirs_equal('input', 'output/input')
  310. info_output = self.cmd('info', self.repository_location + '::test')
  311. item_count = 3 if has_lchflags else 4 # one file is UF_NODUMP
  312. self.assert_in('Number of files: %d' % item_count, info_output)
  313. shutil.rmtree(self.cache_path)
  314. with environment_variable(BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='yes'):
  315. info_output2 = self.cmd('info', self.repository_location + '::test')
  316. def filter(output):
  317. # filter for interesting "info" output, ignore cache rebuilding related stuff
  318. prefixes = ['Name:', 'Fingerprint:', 'Number of files:', 'This archive:',
  319. 'All archives:', 'Chunk index:', ]
  320. result = []
  321. for line in output.splitlines():
  322. for prefix in prefixes:
  323. if line.startswith(prefix):
  324. result.append(line)
  325. return '\n'.join(result)
  326. # the interesting parts of info_output2 and info_output should be same
  327. self.assert_equal(filter(info_output), filter(info_output2))
  328. def test_unix_socket(self):
  329. self.cmd('init', self.repository_location)
  330. sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  331. sock.bind(os.path.join(self.input_path, 'unix-socket'))
  332. self.cmd('create', self.repository_location + '::test', 'input')
  333. sock.close()
  334. with changedir('output'):
  335. self.cmd('extract', self.repository_location + '::test')
  336. assert not os.path.exists('input/unix-socket')
  337. def test_symlink_extract(self):
  338. self.create_test_files()
  339. self.cmd('init', self.repository_location)
  340. self.cmd('create', self.repository_location + '::test', 'input')
  341. with changedir('output'):
  342. self.cmd('extract', self.repository_location + '::test')
  343. assert os.readlink('input/link1') == 'somewhere'
  344. def test_atime(self):
  345. self.create_test_files()
  346. atime, mtime = 123456780, 234567890
  347. os.utime('input/file1', (atime, mtime))
  348. self.cmd('init', self.repository_location)
  349. self.cmd('create', self.repository_location + '::test', 'input')
  350. with changedir('output'):
  351. self.cmd('extract', self.repository_location + '::test')
  352. sti = os.stat('input/file1')
  353. sto = os.stat('output/input/file1')
  354. assert sti.st_mtime_ns == sto.st_mtime_ns == mtime * 1e9
  355. if hasattr(os, 'O_NOATIME'):
  356. assert sti.st_atime_ns == sto.st_atime_ns == atime * 1e9
  357. else:
  358. # it touched the input file's atime while backing it up
  359. assert sto.st_atime_ns == atime * 1e9
  360. def _extract_repository_id(self, path):
  361. with Repository(self.repository_path) as repository:
  362. return repository.id
  363. def _set_repository_id(self, path, id):
  364. config = ConfigParser(interpolation=None)
  365. config.read(os.path.join(path, 'config'))
  366. config.set('repository', 'id', bin_to_hex(id))
  367. with open(os.path.join(path, 'config'), 'w') as fd:
  368. config.write(fd)
  369. with Repository(self.repository_path) as repository:
  370. return repository.id
  371. def test_sparse_file(self):
  372. # no sparse file support on Mac OS X
  373. sparse_support = sys.platform != 'darwin'
  374. filename = os.path.join(self.input_path, 'sparse')
  375. content = b'foobar'
  376. hole_size = 5 * (1 << CHUNK_MAX_EXP) # 5 full chunker buffers
  377. with open(filename, 'wb') as fd:
  378. # create a file that has a hole at the beginning and end (if the
  379. # OS and filesystem supports sparse files)
  380. fd.seek(hole_size, 1)
  381. fd.write(content)
  382. fd.seek(hole_size, 1)
  383. pos = fd.tell()
  384. fd.truncate(pos)
  385. total_len = hole_size + len(content) + hole_size
  386. st = os.stat(filename)
  387. self.assert_equal(st.st_size, total_len)
  388. if sparse_support and hasattr(st, 'st_blocks'):
  389. self.assert_true(st.st_blocks * 512 < total_len / 9) # is input sparse?
  390. self.cmd('init', self.repository_location)
  391. self.cmd('create', self.repository_location + '::test', 'input')
  392. with changedir('output'):
  393. self.cmd('extract', '--sparse', self.repository_location + '::test')
  394. self.assert_dirs_equal('input', 'output/input')
  395. filename = os.path.join(self.output_path, 'input', 'sparse')
  396. with open(filename, 'rb') as fd:
  397. # check if file contents are as expected
  398. self.assert_equal(fd.read(hole_size), b'\0' * hole_size)
  399. self.assert_equal(fd.read(len(content)), content)
  400. self.assert_equal(fd.read(hole_size), b'\0' * hole_size)
  401. st = os.stat(filename)
  402. self.assert_equal(st.st_size, total_len)
  403. if sparse_support and hasattr(st, 'st_blocks'):
  404. self.assert_true(st.st_blocks * 512 < total_len / 9) # is output sparse?
  405. def test_unusual_filenames(self):
  406. filenames = ['normal', 'with some blanks', '(with_parens)', ]
  407. for filename in filenames:
  408. filename = os.path.join(self.input_path, filename)
  409. with open(filename, 'wb'):
  410. pass
  411. self.cmd('init', self.repository_location)
  412. self.cmd('create', self.repository_location + '::test', 'input')
  413. for filename in filenames:
  414. with changedir('output'):
  415. self.cmd('extract', self.repository_location + '::test', os.path.join('input', filename))
  416. assert os.path.exists(os.path.join('output', 'input', filename))
  417. def test_repository_swap_detection(self):
  418. self.create_test_files()
  419. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  420. self.cmd('init', '--encryption=repokey', self.repository_location)
  421. repository_id = self._extract_repository_id(self.repository_path)
  422. self.cmd('create', self.repository_location + '::test', 'input')
  423. shutil.rmtree(self.repository_path)
  424. self.cmd('init', '--encryption=none', self.repository_location)
  425. self._set_repository_id(self.repository_path, repository_id)
  426. self.assert_equal(repository_id, self._extract_repository_id(self.repository_path))
  427. if self.FORK_DEFAULT:
  428. self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR)
  429. else:
  430. self.assert_raises(Cache.EncryptionMethodMismatch, lambda: self.cmd('create', self.repository_location + '::test.2', 'input'))
  431. def test_repository_swap_detection2(self):
  432. self.create_test_files()
  433. self.cmd('init', '--encryption=none', self.repository_location + '_unencrypted')
  434. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  435. self.cmd('init', '--encryption=repokey', self.repository_location + '_encrypted')
  436. self.cmd('create', self.repository_location + '_encrypted::test', 'input')
  437. shutil.rmtree(self.repository_path + '_encrypted')
  438. os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted')
  439. if self.FORK_DEFAULT:
  440. self.cmd('create', self.repository_location + '_encrypted::test.2', 'input', exit_code=EXIT_ERROR)
  441. else:
  442. self.assert_raises(Cache.RepositoryAccessAborted, lambda: self.cmd('create', self.repository_location + '_encrypted::test.2', 'input'))
  443. def test_strip_components(self):
  444. self.cmd('init', self.repository_location)
  445. self.create_regular_file('dir/file')
  446. self.cmd('create', self.repository_location + '::test', 'input')
  447. with changedir('output'):
  448. self.cmd('extract', self.repository_location + '::test', '--strip-components', '3')
  449. self.assert_true(not os.path.exists('file'))
  450. with self.assert_creates_file('file'):
  451. self.cmd('extract', self.repository_location + '::test', '--strip-components', '2')
  452. with self.assert_creates_file('dir/file'):
  453. self.cmd('extract', self.repository_location + '::test', '--strip-components', '1')
  454. with self.assert_creates_file('input/dir/file'):
  455. self.cmd('extract', self.repository_location + '::test', '--strip-components', '0')
  456. def _extract_hardlinks_setup(self):
  457. os.mkdir(os.path.join(self.input_path, 'dir1'))
  458. os.mkdir(os.path.join(self.input_path, 'dir1/subdir'))
  459. self.create_regular_file('source')
  460. os.link(os.path.join(self.input_path, 'source'),
  461. os.path.join(self.input_path, 'abba'))
  462. os.link(os.path.join(self.input_path, 'source'),
  463. os.path.join(self.input_path, 'dir1/hardlink'))
  464. os.link(os.path.join(self.input_path, 'source'),
  465. os.path.join(self.input_path, 'dir1/subdir/hardlink'))
  466. self.create_regular_file('dir1/source2')
  467. os.link(os.path.join(self.input_path, 'dir1/source2'),
  468. os.path.join(self.input_path, 'dir1/aaaa'))
  469. self.cmd('init', self.repository_location)
  470. self.cmd('create', self.repository_location + '::test', 'input')
  471. def test_strip_components_links(self):
  472. self._extract_hardlinks_setup()
  473. with changedir('output'):
  474. self.cmd('extract', self.repository_location + '::test', '--strip-components', '2')
  475. assert os.stat('hardlink').st_nlink == 2
  476. assert os.stat('subdir/hardlink').st_nlink == 2
  477. assert os.stat('aaaa').st_nlink == 2
  478. assert os.stat('source2').st_nlink == 2
  479. with changedir('output'):
  480. self.cmd('extract', self.repository_location + '::test')
  481. assert os.stat('input/dir1/hardlink').st_nlink == 4
  482. def test_extract_hardlinks(self):
  483. self._extract_hardlinks_setup()
  484. with changedir('output'):
  485. self.cmd('extract', self.repository_location + '::test', 'input/dir1')
  486. assert os.stat('input/dir1/hardlink').st_nlink == 2
  487. assert os.stat('input/dir1/subdir/hardlink').st_nlink == 2
  488. assert os.stat('input/dir1/aaaa').st_nlink == 2
  489. assert os.stat('input/dir1/source2').st_nlink == 2
  490. with changedir('output'):
  491. self.cmd('extract', self.repository_location + '::test')
  492. assert os.stat('input/dir1/hardlink').st_nlink == 4
  493. def test_extract_include_exclude(self):
  494. self.cmd('init', self.repository_location)
  495. self.create_regular_file('file1', size=1024 * 80)
  496. self.create_regular_file('file2', size=1024 * 80)
  497. self.create_regular_file('file3', size=1024 * 80)
  498. self.create_regular_file('file4', size=1024 * 80)
  499. self.cmd('create', '--exclude=input/file4', self.repository_location + '::test', 'input')
  500. with changedir('output'):
  501. self.cmd('extract', self.repository_location + '::test', 'input/file1', )
  502. self.assert_equal(sorted(os.listdir('output/input')), ['file1'])
  503. with changedir('output'):
  504. self.cmd('extract', '--exclude=input/file2', self.repository_location + '::test')
  505. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  506. with changedir('output'):
  507. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  508. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  509. def test_extract_include_exclude_regex(self):
  510. self.cmd('init', self.repository_location)
  511. self.create_regular_file('file1', size=1024 * 80)
  512. self.create_regular_file('file2', size=1024 * 80)
  513. self.create_regular_file('file3', size=1024 * 80)
  514. self.create_regular_file('file4', size=1024 * 80)
  515. self.create_regular_file('file333', size=1024 * 80)
  516. # Create with regular expression exclusion for file4
  517. self.cmd('create', '--exclude=re:input/file4$', self.repository_location + '::test', 'input')
  518. with changedir('output'):
  519. self.cmd('extract', self.repository_location + '::test')
  520. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333'])
  521. shutil.rmtree('output/input')
  522. # Extract with regular expression exclusion
  523. with changedir('output'):
  524. self.cmd('extract', '--exclude=re:file3+', self.repository_location + '::test')
  525. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2'])
  526. shutil.rmtree('output/input')
  527. # Combine --exclude with fnmatch and regular expression
  528. with changedir('output'):
  529. self.cmd('extract', '--exclude=input/file2', '--exclude=re:file[01]', self.repository_location + '::test')
  530. self.assert_equal(sorted(os.listdir('output/input')), ['file3', 'file333'])
  531. shutil.rmtree('output/input')
  532. # Combine --exclude-from and regular expression exclusion
  533. with changedir('output'):
  534. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, '--exclude=re:file1',
  535. '--exclude=re:file(\\d)\\1\\1$', self.repository_location + '::test')
  536. self.assert_equal(sorted(os.listdir('output/input')), ['file3'])
  537. def test_extract_include_exclude_regex_from_file(self):
  538. self.cmd('init', self.repository_location)
  539. self.create_regular_file('file1', size=1024 * 80)
  540. self.create_regular_file('file2', size=1024 * 80)
  541. self.create_regular_file('file3', size=1024 * 80)
  542. self.create_regular_file('file4', size=1024 * 80)
  543. self.create_regular_file('file333', size=1024 * 80)
  544. self.create_regular_file('aa:something', size=1024 * 80)
  545. # Create while excluding using mixed pattern styles
  546. with open(self.exclude_file_path, 'wb') as fd:
  547. fd.write(b're:input/file4$\n')
  548. fd.write(b'fm:*aa:*thing\n')
  549. self.cmd('create', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test', 'input')
  550. with changedir('output'):
  551. self.cmd('extract', self.repository_location + '::test')
  552. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333'])
  553. shutil.rmtree('output/input')
  554. # Exclude using regular expression
  555. with open(self.exclude_file_path, 'wb') as fd:
  556. fd.write(b're:file3+\n')
  557. with changedir('output'):
  558. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  559. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2'])
  560. shutil.rmtree('output/input')
  561. # Mixed exclude pattern styles
  562. with open(self.exclude_file_path, 'wb') as fd:
  563. fd.write(b're:file(\\d)\\1\\1$\n')
  564. fd.write(b'fm:nothingwillmatchthis\n')
  565. fd.write(b'*/file1\n')
  566. fd.write(b're:file2$\n')
  567. with changedir('output'):
  568. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  569. self.assert_equal(sorted(os.listdir('output/input')), ['file3'])
  570. def test_extract_with_pattern(self):
  571. self.cmd("init", self.repository_location)
  572. self.create_regular_file("file1", size=1024 * 80)
  573. self.create_regular_file("file2", size=1024 * 80)
  574. self.create_regular_file("file3", size=1024 * 80)
  575. self.create_regular_file("file4", size=1024 * 80)
  576. self.create_regular_file("file333", size=1024 * 80)
  577. self.cmd("create", self.repository_location + "::test", "input")
  578. # Extract everything with regular expression
  579. with changedir("output"):
  580. self.cmd("extract", self.repository_location + "::test", "re:.*")
  581. self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file3", "file333", "file4"])
  582. shutil.rmtree("output/input")
  583. # Extract with pattern while also excluding files
  584. with changedir("output"):
  585. self.cmd("extract", "--exclude=re:file[34]$", self.repository_location + "::test", r"re:file\d$")
  586. self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2"])
  587. shutil.rmtree("output/input")
  588. # Combine --exclude with pattern for extraction
  589. with changedir("output"):
  590. self.cmd("extract", "--exclude=input/file1", self.repository_location + "::test", "re:file[12]$")
  591. self.assert_equal(sorted(os.listdir("output/input")), ["file2"])
  592. shutil.rmtree("output/input")
  593. # Multiple pattern
  594. with changedir("output"):
  595. self.cmd("extract", self.repository_location + "::test", "fm:input/file1", "fm:*file33*", "input/file2")
  596. self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file333"])
  597. def test_extract_list_output(self):
  598. self.cmd('init', self.repository_location)
  599. self.create_regular_file('file', size=1024 * 80)
  600. self.cmd('create', self.repository_location + '::test', 'input')
  601. with changedir('output'):
  602. output = self.cmd('extract', self.repository_location + '::test')
  603. self.assert_not_in("input/file", output)
  604. shutil.rmtree('output/input')
  605. with changedir('output'):
  606. output = self.cmd('extract', '--info', self.repository_location + '::test')
  607. self.assert_not_in("input/file", output)
  608. shutil.rmtree('output/input')
  609. with changedir('output'):
  610. output = self.cmd('extract', '--list', self.repository_location + '::test')
  611. self.assert_in("input/file", output)
  612. shutil.rmtree('output/input')
  613. with changedir('output'):
  614. output = self.cmd('extract', '--list', '--info', self.repository_location + '::test')
  615. self.assert_in("input/file", output)
  616. def _create_test_caches(self):
  617. self.cmd('init', self.repository_location)
  618. self.create_regular_file('file1', size=1024 * 80)
  619. self.create_regular_file('cache1/%s' % CACHE_TAG_NAME,
  620. contents=CACHE_TAG_CONTENTS + b' extra stuff')
  621. self.create_regular_file('cache2/%s' % CACHE_TAG_NAME,
  622. contents=b'invalid signature')
  623. os.mkdir('input/cache3')
  624. os.link('input/cache1/%s' % CACHE_TAG_NAME, 'input/cache3/%s' % CACHE_TAG_NAME)
  625. def _assert_test_caches(self):
  626. with changedir('output'):
  627. self.cmd('extract', self.repository_location + '::test')
  628. self.assert_equal(sorted(os.listdir('output/input')), ['cache2', 'file1'])
  629. self.assert_equal(sorted(os.listdir('output/input/cache2')), [CACHE_TAG_NAME])
  630. def test_exclude_caches(self):
  631. self._create_test_caches()
  632. self.cmd('create', '--exclude-caches', self.repository_location + '::test', 'input')
  633. self._assert_test_caches()
  634. def test_recreate_exclude_caches(self):
  635. self._create_test_caches()
  636. self.cmd('create', self.repository_location + '::test', 'input')
  637. self.cmd('recreate', '--exclude-caches', self.repository_location + '::test')
  638. self._assert_test_caches()
  639. def _create_test_tagged(self):
  640. self.cmd('init', self.repository_location)
  641. self.create_regular_file('file1', size=1024 * 80)
  642. self.create_regular_file('tagged1/.NOBACKUP')
  643. self.create_regular_file('tagged2/00-NOBACKUP')
  644. self.create_regular_file('tagged3/.NOBACKUP/file2')
  645. def _assert_test_tagged(self):
  646. with changedir('output'):
  647. self.cmd('extract', self.repository_location + '::test')
  648. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'tagged3'])
  649. def test_exclude_tagged(self):
  650. self._create_test_tagged()
  651. self.cmd('create', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP', self.repository_location + '::test', 'input')
  652. self._assert_test_tagged()
  653. def test_recreate_exclude_tagged(self):
  654. self._create_test_tagged()
  655. self.cmd('create', self.repository_location + '::test', 'input')
  656. self.cmd('recreate', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP',
  657. self.repository_location + '::test')
  658. self._assert_test_tagged()
  659. def _create_test_keep_tagged(self):
  660. self.cmd('init', self.repository_location)
  661. self.create_regular_file('file0', size=1024)
  662. self.create_regular_file('tagged1/.NOBACKUP1')
  663. self.create_regular_file('tagged1/file1', size=1024)
  664. self.create_regular_file('tagged2/.NOBACKUP2')
  665. self.create_regular_file('tagged2/file2', size=1024)
  666. self.create_regular_file('tagged3/%s' % CACHE_TAG_NAME,
  667. contents=CACHE_TAG_CONTENTS + b' extra stuff')
  668. self.create_regular_file('tagged3/file3', size=1024)
  669. self.create_regular_file('taggedall/.NOBACKUP1')
  670. self.create_regular_file('taggedall/.NOBACKUP2')
  671. self.create_regular_file('taggedall/%s' % CACHE_TAG_NAME,
  672. contents=CACHE_TAG_CONTENTS + b' extra stuff')
  673. self.create_regular_file('taggedall/file4', size=1024)
  674. def _assert_test_keep_tagged(self):
  675. with changedir('output'):
  676. self.cmd('extract', self.repository_location + '::test')
  677. self.assert_equal(sorted(os.listdir('output/input')), ['file0', 'tagged1', 'tagged2', 'tagged3', 'taggedall'])
  678. self.assert_equal(os.listdir('output/input/tagged1'), ['.NOBACKUP1'])
  679. self.assert_equal(os.listdir('output/input/tagged2'), ['.NOBACKUP2'])
  680. self.assert_equal(os.listdir('output/input/tagged3'), [CACHE_TAG_NAME])
  681. self.assert_equal(sorted(os.listdir('output/input/taggedall')),
  682. ['.NOBACKUP1', '.NOBACKUP2', CACHE_TAG_NAME, ])
  683. def test_exclude_keep_tagged(self):
  684. self._create_test_keep_tagged()
  685. self.cmd('create', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2',
  686. '--exclude-caches', '--keep-tag-files', self.repository_location + '::test', 'input')
  687. self._assert_test_keep_tagged()
  688. def test_recreate_exclude_keep_tagged(self):
  689. self._create_test_keep_tagged()
  690. self.cmd('create', self.repository_location + '::test', 'input')
  691. self.cmd('recreate', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2',
  692. '--exclude-caches', '--keep-tag-files', self.repository_location + '::test')
  693. self._assert_test_keep_tagged()
  694. @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='Linux capabilities test, requires fakeroot >= 1.20.2')
  695. def test_extract_capabilities(self):
  696. fchown = os.fchown
  697. # We need to manually patch chown to get the behaviour Linux has, since fakeroot does not
  698. # accurately model the interaction of chown(2) and Linux capabilities, i.e. it does not remove them.
  699. def patched_fchown(fd, uid, gid):
  700. xattr.setxattr(fd, 'security.capability', None, follow_symlinks=False)
  701. fchown(fd, uid, gid)
  702. # The capability descriptor used here is valid and taken from a /usr/bin/ping
  703. capabilities = b'\x01\x00\x00\x02\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
  704. self.create_regular_file('file')
  705. xattr.setxattr('input/file', 'security.capability', capabilities)
  706. self.cmd('init', self.repository_location)
  707. self.cmd('create', self.repository_location + '::test', 'input')
  708. with changedir('output'):
  709. with patch.object(os, 'fchown', patched_fchown):
  710. self.cmd('extract', self.repository_location + '::test')
  711. assert xattr.getxattr('input/file', 'security.capability') == capabilities
  712. def test_path_normalization(self):
  713. self.cmd('init', self.repository_location)
  714. self.create_regular_file('dir1/dir2/file', size=1024 * 80)
  715. with changedir('input/dir1/dir2'):
  716. self.cmd('create', self.repository_location + '::test', '../../../input/dir1/../dir1/dir2/..')
  717. output = self.cmd('list', self.repository_location + '::test')
  718. self.assert_not_in('..', output)
  719. self.assert_in(' input/dir1/dir2/file', output)
  720. def test_exclude_normalization(self):
  721. self.cmd('init', self.repository_location)
  722. self.create_regular_file('file1', size=1024 * 80)
  723. self.create_regular_file('file2', size=1024 * 80)
  724. with changedir('input'):
  725. self.cmd('create', '--exclude=file1', self.repository_location + '::test1', '.')
  726. with changedir('output'):
  727. self.cmd('extract', self.repository_location + '::test1')
  728. self.assert_equal(sorted(os.listdir('output')), ['file2'])
  729. with changedir('input'):
  730. self.cmd('create', '--exclude=./file1', self.repository_location + '::test2', '.')
  731. with changedir('output'):
  732. self.cmd('extract', self.repository_location + '::test2')
  733. self.assert_equal(sorted(os.listdir('output')), ['file2'])
  734. self.cmd('create', '--exclude=input/./file1', self.repository_location + '::test3', 'input')
  735. with changedir('output'):
  736. self.cmd('extract', self.repository_location + '::test3')
  737. self.assert_equal(sorted(os.listdir('output/input')), ['file2'])
  738. def test_repeated_files(self):
  739. self.create_regular_file('file1', size=1024 * 80)
  740. self.cmd('init', self.repository_location)
  741. self.cmd('create', self.repository_location + '::test', 'input', 'input')
  742. def test_overwrite(self):
  743. self.create_regular_file('file1', size=1024 * 80)
  744. self.create_regular_file('dir2/file2', size=1024 * 80)
  745. self.cmd('init', self.repository_location)
  746. self.cmd('create', self.repository_location + '::test', 'input')
  747. # Overwriting regular files and directories should be supported
  748. os.mkdir('output/input')
  749. os.mkdir('output/input/file1')
  750. os.mkdir('output/input/dir2')
  751. with changedir('output'):
  752. self.cmd('extract', self.repository_location + '::test')
  753. self.assert_dirs_equal('input', 'output/input')
  754. # But non-empty dirs should fail
  755. os.unlink('output/input/file1')
  756. os.mkdir('output/input/file1')
  757. os.mkdir('output/input/file1/dir')
  758. with changedir('output'):
  759. self.cmd('extract', self.repository_location + '::test', exit_code=1)
  760. def test_rename(self):
  761. self.create_regular_file('file1', size=1024 * 80)
  762. self.create_regular_file('dir2/file2', size=1024 * 80)
  763. self.cmd('init', self.repository_location)
  764. self.cmd('create', self.repository_location + '::test', 'input')
  765. self.cmd('create', self.repository_location + '::test.2', 'input')
  766. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  767. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  768. self.cmd('rename', self.repository_location + '::test', 'test.3')
  769. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  770. self.cmd('rename', self.repository_location + '::test.2', 'test.4')
  771. self.cmd('extract', '--dry-run', self.repository_location + '::test.3')
  772. self.cmd('extract', '--dry-run', self.repository_location + '::test.4')
  773. # Make sure both archives have been renamed
  774. with Repository(self.repository_path) as repository:
  775. manifest, key = Manifest.load(repository)
  776. self.assert_equal(len(manifest.archives), 2)
  777. self.assert_in('test.3', manifest.archives)
  778. self.assert_in('test.4', manifest.archives)
  779. def test_comment(self):
  780. self.create_regular_file('file1', size=1024 * 80)
  781. self.cmd('init', self.repository_location)
  782. self.cmd('create', self.repository_location + '::test1', 'input')
  783. self.cmd('create', '--comment', 'this is the comment', self.repository_location + '::test2', 'input')
  784. self.cmd('create', '--comment', '"deleted" comment', self.repository_location + '::test3', 'input')
  785. self.cmd('create', '--comment', 'preserved comment', self.repository_location + '::test4', 'input')
  786. assert 'Comment: \n' in self.cmd('info', self.repository_location + '::test1')
  787. assert 'Comment: this is the comment' in self.cmd('info', self.repository_location + '::test2')
  788. self.cmd('recreate', self.repository_location + '::test1', '--comment', 'added comment')
  789. self.cmd('recreate', self.repository_location + '::test2', '--comment', 'modified comment')
  790. self.cmd('recreate', self.repository_location + '::test3', '--comment', '')
  791. self.cmd('recreate', self.repository_location + '::test4', '12345')
  792. assert 'Comment: added comment' in self.cmd('info', self.repository_location + '::test1')
  793. assert 'Comment: modified comment' in self.cmd('info', self.repository_location + '::test2')
  794. assert 'Comment: \n' in self.cmd('info', self.repository_location + '::test3')
  795. assert 'Comment: preserved comment' in self.cmd('info', self.repository_location + '::test4')
  796. def test_delete(self):
  797. self.create_regular_file('file1', size=1024 * 80)
  798. self.create_regular_file('dir2/file2', size=1024 * 80)
  799. self.cmd('init', self.repository_location)
  800. self.cmd('create', self.repository_location + '::test', 'input')
  801. self.cmd('create', self.repository_location + '::test.2', 'input')
  802. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  803. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  804. self.cmd('delete', self.repository_location + '::test')
  805. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  806. output = self.cmd('delete', '--stats', self.repository_location + '::test.2')
  807. self.assert_in('Deleted data:', output)
  808. # Make sure all data except the manifest has been deleted
  809. with Repository(self.repository_path) as repository:
  810. self.assert_equal(len(repository), 1)
  811. def test_delete_repo(self):
  812. self.create_regular_file('file1', size=1024 * 80)
  813. self.create_regular_file('dir2/file2', size=1024 * 80)
  814. self.cmd('init', self.repository_location)
  815. self.cmd('create', self.repository_location + '::test', 'input')
  816. self.cmd('create', self.repository_location + '::test.2', 'input')
  817. os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'no'
  818. self.cmd('delete', self.repository_location, exit_code=2)
  819. assert os.path.exists(self.repository_path)
  820. os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
  821. self.cmd('delete', self.repository_location)
  822. # Make sure the repo is gone
  823. self.assertFalse(os.path.exists(self.repository_path))
  824. def test_corrupted_repository(self):
  825. self.cmd('init', self.repository_location)
  826. self.create_src_archive('test')
  827. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  828. output = self.cmd('check', '--show-version', self.repository_location)
  829. self.assert_in('borgbackup version', output) # implied output even without --info given
  830. self.assert_not_in('Starting repository check', output) # --info not given for root logger
  831. name = sorted(os.listdir(os.path.join(self.tmpdir, 'repository', 'data', '0')), reverse=True)[1]
  832. with open(os.path.join(self.tmpdir, 'repository', 'data', '0', name), 'r+b') as fd:
  833. fd.seek(100)
  834. fd.write(b'XXXX')
  835. output = self.cmd('check', '--info', self.repository_location, exit_code=1)
  836. self.assert_in('Starting repository check', output) # --info given for root logger
  837. # we currently need to be able to create a lock directory inside the repo:
  838. @pytest.mark.xfail(reason="we need to be able to create the lock directory inside the repo")
  839. def test_readonly_repository(self):
  840. self.cmd('init', self.repository_location)
  841. self.create_src_archive('test')
  842. os.system('chmod -R ugo-w ' + self.repository_path)
  843. try:
  844. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  845. finally:
  846. # Restore permissions so shutil.rmtree is able to delete it
  847. os.system('chmod -R u+w ' + self.repository_path)
  848. def test_umask(self):
  849. self.create_regular_file('file1', size=1024 * 80)
  850. self.cmd('init', self.repository_location)
  851. self.cmd('create', self.repository_location + '::test', 'input')
  852. mode = os.stat(self.repository_path).st_mode
  853. self.assertEqual(stat.S_IMODE(mode), 0o700)
  854. def test_create_dry_run(self):
  855. self.cmd('init', self.repository_location)
  856. self.cmd('create', '--dry-run', self.repository_location + '::test', 'input')
  857. # Make sure no archive has been created
  858. with Repository(self.repository_path) as repository:
  859. manifest, key = Manifest.load(repository)
  860. self.assert_equal(len(manifest.archives), 0)
  861. def test_progress(self):
  862. self.create_regular_file('file1', size=1024 * 80)
  863. self.cmd('init', self.repository_location)
  864. # progress forced on
  865. output = self.cmd('create', '--progress', self.repository_location + '::test4', 'input')
  866. self.assert_in("\r", output)
  867. # progress forced off
  868. output = self.cmd('create', self.repository_location + '::test5', 'input')
  869. self.assert_not_in("\r", output)
  870. def test_file_status(self):
  871. """test that various file status show expected results
  872. clearly incomplete: only tests for the weird "unchanged" status for now"""
  873. now = time.time()
  874. self.create_regular_file('file1', size=1024 * 80)
  875. os.utime('input/file1', (now - 5, now - 5)) # 5 seconds ago
  876. self.create_regular_file('file2', size=1024 * 80)
  877. self.cmd('init', self.repository_location)
  878. output = self.cmd('create', '--list', self.repository_location + '::test', 'input')
  879. self.assert_in("A input/file1", output)
  880. self.assert_in("A input/file2", output)
  881. # should find first file as unmodified
  882. output = self.cmd('create', '--list', self.repository_location + '::test1', 'input')
  883. self.assert_in("U input/file1", output)
  884. # this is expected, although surprising, for why, see:
  885. # https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file
  886. self.assert_in("A input/file2", output)
  887. def test_file_status_excluded(self):
  888. """test that excluded paths are listed"""
  889. now = time.time()
  890. self.create_regular_file('file1', size=1024 * 80)
  891. os.utime('input/file1', (now - 5, now - 5)) # 5 seconds ago
  892. self.create_regular_file('file2', size=1024 * 80)
  893. if has_lchflags:
  894. self.create_regular_file('file3', size=1024 * 80)
  895. platform.set_flags(os.path.join(self.input_path, 'file3'), stat.UF_NODUMP)
  896. self.cmd('init', self.repository_location)
  897. output = self.cmd('create', '--list', self.repository_location + '::test', 'input')
  898. self.assert_in("A input/file1", output)
  899. self.assert_in("A input/file2", output)
  900. if has_lchflags:
  901. self.assert_in("x input/file3", output)
  902. # should find second file as excluded
  903. output = self.cmd('create', '--list', self.repository_location + '::test1', 'input', '--exclude', '*/file2')
  904. self.assert_in("U input/file1", output)
  905. self.assert_in("x input/file2", output)
  906. if has_lchflags:
  907. self.assert_in("x input/file3", output)
  908. def test_create_topical(self):
  909. now = time.time()
  910. self.create_regular_file('file1', size=1024 * 80)
  911. os.utime('input/file1', (now-5, now-5))
  912. self.create_regular_file('file2', size=1024 * 80)
  913. self.cmd('init', self.repository_location)
  914. # no listing by default
  915. output = self.cmd('create', self.repository_location + '::test', 'input')
  916. self.assert_not_in('file1', output)
  917. # shouldn't be listed even if unchanged
  918. output = self.cmd('create', self.repository_location + '::test0', 'input')
  919. self.assert_not_in('file1', output)
  920. # should list the file as unchanged
  921. output = self.cmd('create', '--list', '--filter=U', self.repository_location + '::test1', 'input')
  922. self.assert_in('file1', output)
  923. # should *not* list the file as changed
  924. output = self.cmd('create', '--list', '--filter=AM', self.repository_location + '::test2', 'input')
  925. self.assert_not_in('file1', output)
  926. # change the file
  927. self.create_regular_file('file1', size=1024 * 100)
  928. # should list the file as changed
  929. output = self.cmd('create', '--list', '--filter=AM', self.repository_location + '::test3', 'input')
  930. self.assert_in('file1', output)
  931. # def test_cmdline_compatibility(self):
  932. # self.create_regular_file('file1', size=1024 * 80)
  933. # self.cmd('init', self.repository_location)
  934. # self.cmd('create', self.repository_location + '::test', 'input')
  935. # output = self.cmd('foo', self.repository_location, '--old')
  936. # self.assert_in('"--old" has been deprecated. Use "--new" instead', output)
  937. def test_prune_repository(self):
  938. self.cmd('init', self.repository_location)
  939. self.cmd('create', self.repository_location + '::test1', src_dir)
  940. self.cmd('create', self.repository_location + '::test2', src_dir)
  941. # these are not really a checkpoints, but they look like some:
  942. self.cmd('create', self.repository_location + '::test3.checkpoint', src_dir)
  943. self.cmd('create', self.repository_location + '::test3.checkpoint.1', src_dir)
  944. self.cmd('create', self.repository_location + '::test4.checkpoint', src_dir)
  945. output = self.cmd('prune', '--list', '--dry-run', self.repository_location, '--keep-daily=2')
  946. self.assert_in('Keeping archive: test2', output)
  947. self.assert_in('Would prune: test1', output)
  948. # must keep the latest non-checkpoint archive:
  949. self.assert_in('Keeping archive: test2', output)
  950. # must keep the latest checkpoint archive:
  951. self.assert_in('Keeping archive: test4.checkpoint', output)
  952. output = self.cmd('list', self.repository_location)
  953. self.assert_in('test1', output)
  954. self.assert_in('test2', output)
  955. self.assert_in('test3.checkpoint', output)
  956. self.assert_in('test3.checkpoint.1', output)
  957. self.assert_in('test4.checkpoint', output)
  958. self.cmd('prune', self.repository_location, '--keep-daily=2')
  959. output = self.cmd('list', self.repository_location)
  960. self.assert_not_in('test1', output)
  961. # the latest non-checkpoint archive must be still there:
  962. self.assert_in('test2', output)
  963. # only the latest checkpoint archive must still be there:
  964. self.assert_not_in('test3.checkpoint', output)
  965. self.assert_not_in('test3.checkpoint.1', output)
  966. self.assert_in('test4.checkpoint', output)
  967. # now we supercede the latest checkpoint by a successful backup:
  968. self.cmd('create', self.repository_location + '::test5', src_dir)
  969. self.cmd('prune', self.repository_location, '--keep-daily=2')
  970. output = self.cmd('list', self.repository_location)
  971. # all checkpoints should be gone now:
  972. self.assert_not_in('checkpoint', output)
  973. # the latest archive must be still there
  974. self.assert_in('test5', output)
  975. def test_prune_repository_save_space(self):
  976. self.cmd('init', self.repository_location)
  977. self.cmd('create', self.repository_location + '::test1', src_dir)
  978. self.cmd('create', self.repository_location + '::test2', src_dir)
  979. output = self.cmd('prune', '--list', '--stats', '--dry-run', self.repository_location, '--keep-daily=2')
  980. self.assert_in('Keeping archive: test2', output)
  981. self.assert_in('Would prune: test1', output)
  982. self.assert_in('Deleted data:', output)
  983. output = self.cmd('list', self.repository_location)
  984. self.assert_in('test1', output)
  985. self.assert_in('test2', output)
  986. self.cmd('prune', '--save-space', self.repository_location, '--keep-daily=2')
  987. output = self.cmd('list', self.repository_location)
  988. self.assert_not_in('test1', output)
  989. self.assert_in('test2', output)
  990. def test_prune_repository_prefix(self):
  991. self.cmd('init', self.repository_location)
  992. self.cmd('create', self.repository_location + '::foo-2015-08-12-10:00', src_dir)
  993. self.cmd('create', self.repository_location + '::foo-2015-08-12-20:00', src_dir)
  994. self.cmd('create', self.repository_location + '::bar-2015-08-12-10:00', src_dir)
  995. self.cmd('create', self.repository_location + '::bar-2015-08-12-20:00', src_dir)
  996. output = self.cmd('prune', '--list', '--dry-run', self.repository_location, '--keep-daily=2', '--prefix=foo-')
  997. self.assert_in('Keeping archive: foo-2015-08-12-20:00', output)
  998. self.assert_in('Would prune: foo-2015-08-12-10:00', output)
  999. output = self.cmd('list', self.repository_location)
  1000. self.assert_in('foo-2015-08-12-10:00', output)
  1001. self.assert_in('foo-2015-08-12-20:00', output)
  1002. self.assert_in('bar-2015-08-12-10:00', output)
  1003. self.assert_in('bar-2015-08-12-20:00', output)
  1004. self.cmd('prune', self.repository_location, '--keep-daily=2', '--prefix=foo-')
  1005. output = self.cmd('list', self.repository_location)
  1006. self.assert_not_in('foo-2015-08-12-10:00', output)
  1007. self.assert_in('foo-2015-08-12-20:00', output)
  1008. self.assert_in('bar-2015-08-12-10:00', output)
  1009. self.assert_in('bar-2015-08-12-20:00', output)
  1010. def test_list_prefix(self):
  1011. self.cmd('init', self.repository_location)
  1012. self.cmd('create', self.repository_location + '::test-1', src_dir)
  1013. self.cmd('create', self.repository_location + '::something-else-than-test-1', src_dir)
  1014. self.cmd('create', self.repository_location + '::test-2', src_dir)
  1015. output = self.cmd('list', '--prefix=test-', self.repository_location)
  1016. self.assert_in('test-1', output)
  1017. self.assert_in('test-2', output)
  1018. self.assert_not_in('something-else', output)
  1019. def test_list_format(self):
  1020. self.cmd('init', self.repository_location)
  1021. test_archive = self.repository_location + '::test'
  1022. self.cmd('create', test_archive, src_dir)
  1023. self.cmd('list', '--list-format', '-', test_archive, exit_code=1)
  1024. self.archiver.exit_code = 0 # reset exit code for following tests
  1025. output_1 = self.cmd('list', test_archive)
  1026. output_2 = self.cmd('list', '--format', '{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}', test_archive)
  1027. output_3 = self.cmd('list', '--format', '{mtime:%s} {path}{NL}', test_archive)
  1028. self.assertEqual(output_1, output_2)
  1029. self.assertNotEqual(output_1, output_3)
  1030. def test_list_hash(self):
  1031. self.create_regular_file('empty_file', size=0)
  1032. self.create_regular_file('amb', contents=b'a' * 1000000)
  1033. self.cmd('init', self.repository_location)
  1034. test_archive = self.repository_location + '::test'
  1035. self.cmd('create', test_archive, 'input')
  1036. output = self.cmd('list', '--format', '{sha256} {path}{NL}', test_archive)
  1037. assert "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0 input/amb" in output
  1038. assert "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 input/empty_file" in output
  1039. def test_list_chunk_counts(self):
  1040. self.create_regular_file('empty_file', size=0)
  1041. self.create_regular_file('two_chunks')
  1042. with open(os.path.join(self.input_path, 'two_chunks'), 'wb') as fd:
  1043. fd.write(b'abba' * 2000000)
  1044. fd.write(b'baab' * 2000000)
  1045. self.cmd('init', self.repository_location)
  1046. test_archive = self.repository_location + '::test'
  1047. self.cmd('create', test_archive, 'input')
  1048. output = self.cmd('list', '--format', '{num_chunks} {unique_chunks} {path}{NL}', test_archive)
  1049. assert "0 0 input/empty_file" in output
  1050. assert "2 2 input/two_chunks" in output
  1051. def test_list_size(self):
  1052. self.create_regular_file('compressible_file', size=10000)
  1053. self.cmd('init', self.repository_location)
  1054. test_archive = self.repository_location + '::test'
  1055. self.cmd('create', '-C', 'lz4', test_archive, 'input')
  1056. output = self.cmd('list', '--format', '{size} {csize} {path}{NL}', test_archive)
  1057. size, csize, path = output.split("\n")[1].split(" ")
  1058. assert int(csize) < int(size)
  1059. def _get_sizes(self, compression, compressible, size=10000):
  1060. if compressible:
  1061. contents = b'X' * size
  1062. else:
  1063. contents = os.urandom(size)
  1064. self.create_regular_file('file', contents=contents)
  1065. self.cmd('init', '--encryption=none', self.repository_location)
  1066. archive = self.repository_location + '::test'
  1067. self.cmd('create', '-C', compression, archive, 'input')
  1068. output = self.cmd('list', '--format', '{size} {csize} {path}{NL}', archive)
  1069. size, csize, path = output.split("\n")[1].split(" ")
  1070. return int(size), int(csize)
  1071. def test_compression_none_compressible(self):
  1072. size, csize = self._get_sizes('none', compressible=True)
  1073. assert csize >= size
  1074. assert csize == size + 3
  1075. def test_compression_none_uncompressible(self):
  1076. size, csize = self._get_sizes('none', compressible=False)
  1077. assert csize >= size
  1078. assert csize == size + 3
  1079. def test_compression_zlib_compressible(self):
  1080. size, csize = self._get_sizes('zlib', compressible=True)
  1081. assert csize < size * 0.1
  1082. assert csize == 35
  1083. def test_compression_zlib_uncompressible(self):
  1084. size, csize = self._get_sizes('zlib', compressible=False)
  1085. assert csize >= size
  1086. def test_compression_auto_compressible(self):
  1087. size, csize = self._get_sizes('auto,zlib', compressible=True)
  1088. assert csize < size * 0.1
  1089. assert csize == 35 # same as compression 'zlib'
  1090. def test_compression_auto_uncompressible(self):
  1091. size, csize = self._get_sizes('auto,zlib', compressible=False)
  1092. assert csize >= size
  1093. assert csize == size + 3 # same as compression 'none'
  1094. def test_compression_lz4_compressible(self):
  1095. size, csize = self._get_sizes('lz4', compressible=True)
  1096. assert csize < size * 0.1
  1097. def test_compression_lz4_uncompressible(self):
  1098. size, csize = self._get_sizes('lz4', compressible=False)
  1099. assert csize >= size
  1100. def test_compression_lzma_compressible(self):
  1101. size, csize = self._get_sizes('lzma', compressible=True)
  1102. assert csize < size * 0.1
  1103. def test_compression_lzma_uncompressible(self):
  1104. size, csize = self._get_sizes('lzma', compressible=False)
  1105. assert csize >= size
  1106. def test_break_lock(self):
  1107. self.cmd('init', self.repository_location)
  1108. self.cmd('break-lock', self.repository_location)
  1109. def test_usage(self):
  1110. if self.FORK_DEFAULT:
  1111. self.cmd(exit_code=0)
  1112. self.cmd('-h', exit_code=0)
  1113. else:
  1114. self.assert_raises(SystemExit, lambda: self.cmd())
  1115. self.assert_raises(SystemExit, lambda: self.cmd('-h'))
  1116. def test_help(self):
  1117. assert 'Borg' in self.cmd('help')
  1118. assert 'patterns' in self.cmd('help', 'patterns')
  1119. assert 'Initialize' in self.cmd('help', 'init')
  1120. assert 'positional arguments' not in self.cmd('help', 'init', '--epilog-only')
  1121. assert 'This command initializes' not in self.cmd('help', 'init', '--usage-only')
  1122. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  1123. def test_fuse_mount_repository(self):
  1124. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  1125. os.mkdir(mountpoint)
  1126. self.cmd('init', self.repository_location)
  1127. self.create_test_files()
  1128. self.cmd('create', self.repository_location + '::archive', 'input')
  1129. self.cmd('create', self.repository_location + '::archive2', 'input')
  1130. try:
  1131. self.cmd('mount', self.repository_location, mountpoint, fork=True)
  1132. self.wait_for_mount(mountpoint)
  1133. if has_lchflags:
  1134. # remove the file we did not backup, so input and output become equal
  1135. os.remove(os.path.join('input', 'flagfile'))
  1136. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive', 'input'))
  1137. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive2', 'input'))
  1138. finally:
  1139. if sys.platform.startswith('linux'):
  1140. os.system('fusermount -u ' + mountpoint)
  1141. else:
  1142. os.system('umount ' + mountpoint)
  1143. os.rmdir(mountpoint)
  1144. # Give the daemon some time to exit
  1145. time.sleep(.2)
  1146. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  1147. def test_fuse_mount_archive(self):
  1148. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  1149. os.mkdir(mountpoint)
  1150. self.cmd('init', self.repository_location)
  1151. self.create_test_files()
  1152. self.cmd('create', self.repository_location + '::archive', 'input')
  1153. try:
  1154. self.cmd('mount', self.repository_location + '::archive', mountpoint, fork=True)
  1155. self.wait_for_mount(mountpoint)
  1156. if has_lchflags:
  1157. # remove the file we did not backup, so input and output become equal
  1158. os.remove(os.path.join('input', 'flagfile'))
  1159. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'input'))
  1160. finally:
  1161. if sys.platform.startswith('linux'):
  1162. os.system('fusermount -u ' + mountpoint)
  1163. else:
  1164. os.system('umount ' + mountpoint)
  1165. os.rmdir(mountpoint)
  1166. # Give the daemon some time to exit
  1167. time.sleep(.2)
  1168. def verify_aes_counter_uniqueness(self, method):
  1169. seen = set() # Chunks already seen
  1170. used = set() # counter values already used
  1171. def verify_uniqueness():
  1172. with Repository(self.repository_path) as repository:
  1173. for key, _ in repository.open_index(repository.get_transaction_id()).iteritems():
  1174. data = repository.get(key)
  1175. hash = sha256(data).digest()
  1176. if hash not in seen:
  1177. seen.add(hash)
  1178. num_blocks = num_aes_blocks(len(data) - 41)
  1179. nonce = bytes_to_long(data[33:41])
  1180. for counter in range(nonce, nonce + num_blocks):
  1181. self.assert_not_in(counter, used)
  1182. used.add(counter)
  1183. self.create_test_files()
  1184. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  1185. self.cmd('init', '--encryption=' + method, self.repository_location)
  1186. verify_uniqueness()
  1187. self.cmd('create', self.repository_location + '::test', 'input')
  1188. verify_uniqueness()
  1189. self.cmd('create', self.repository_location + '::test.2', 'input')
  1190. verify_uniqueness()
  1191. self.cmd('delete', self.repository_location + '::test.2')
  1192. verify_uniqueness()
  1193. self.assert_equal(used, set(range(len(used))))
  1194. def test_aes_counter_uniqueness_keyfile(self):
  1195. self.verify_aes_counter_uniqueness('keyfile')
  1196. def test_aes_counter_uniqueness_passphrase(self):
  1197. self.verify_aes_counter_uniqueness('repokey')
  1198. def test_debug_dump_archive_items(self):
  1199. self.create_test_files()
  1200. self.cmd('init', self.repository_location)
  1201. self.cmd('create', self.repository_location + '::test', 'input')
  1202. with changedir('output'):
  1203. output = self.cmd('debug-dump-archive-items', self.repository_location + '::test')
  1204. output_dir = sorted(os.listdir('output'))
  1205. assert len(output_dir) > 0 and output_dir[0].startswith('000000_')
  1206. assert 'Done.' in output
  1207. def test_debug_put_get_delete_obj(self):
  1208. self.cmd('init', self.repository_location)
  1209. data = b'some data'
  1210. hexkey = sha256(data).hexdigest()
  1211. self.create_regular_file('file', contents=data)
  1212. output = self.cmd('debug-put-obj', self.repository_location, 'input/file')
  1213. assert hexkey in output
  1214. output = self.cmd('debug-get-obj', self.repository_location, hexkey, 'output/file')
  1215. assert hexkey in output
  1216. with open('output/file', 'rb') as f:
  1217. data_read = f.read()
  1218. assert data == data_read
  1219. output = self.cmd('debug-delete-obj', self.repository_location, hexkey)
  1220. assert "deleted" in output
  1221. output = self.cmd('debug-delete-obj', self.repository_location, hexkey)
  1222. assert "not found" in output
  1223. output = self.cmd('debug-delete-obj', self.repository_location, 'invalid')
  1224. assert "is invalid" in output
  1225. def test_init_interrupt(self):
  1226. def raise_eof(*args):
  1227. raise EOFError
  1228. with patch.object(KeyfileKeyBase, 'create', raise_eof):
  1229. self.cmd('init', self.repository_location, exit_code=1)
  1230. assert not os.path.exists(self.repository_location)
  1231. def test_recreate_basic(self):
  1232. self.create_test_files()
  1233. self.create_regular_file('dir2/file3', size=1024 * 80)
  1234. self.cmd('init', self.repository_location)
  1235. archive = self.repository_location + '::test0'
  1236. self.cmd('create', archive, 'input')
  1237. self.cmd('recreate', archive, 'input/dir2', '-e', 'input/dir2/file3')
  1238. listing = self.cmd('list', '--short', archive)
  1239. assert 'file1' not in listing
  1240. assert 'dir2/file2' in listing
  1241. assert 'dir2/file3' not in listing
  1242. def test_recreate_subtree_hardlinks(self):
  1243. # This is essentially the same problem set as in test_extract_hardlinks
  1244. self._extract_hardlinks_setup()
  1245. self.cmd('create', self.repository_location + '::test2', 'input')
  1246. self.cmd('recreate', self.repository_location + '::test', 'input/dir1')
  1247. with changedir('output'):
  1248. self.cmd('extract', self.repository_location + '::test')
  1249. assert os.stat('input/dir1/hardlink').st_nlink == 2
  1250. assert os.stat('input/dir1/subdir/hardlink').st_nlink == 2
  1251. assert os.stat('input/dir1/aaaa').st_nlink == 2
  1252. assert os.stat('input/dir1/source2').st_nlink == 2
  1253. with changedir('output'):
  1254. self.cmd('extract', self.repository_location + '::test2')
  1255. assert os.stat('input/dir1/hardlink').st_nlink == 4
  1256. def test_recreate_rechunkify(self):
  1257. with open(os.path.join(self.input_path, 'large_file'), 'wb') as fd:
  1258. fd.write(b'a' * 280)
  1259. fd.write(b'b' * 280)
  1260. self.cmd('init', self.repository_location)
  1261. self.cmd('create', '--chunker-params', '7,9,8,128', self.repository_location + '::test1', 'input')
  1262. self.cmd('create', self.repository_location + '::test2', 'input', '--no-files-cache')
  1263. list = self.cmd('list', self.repository_location + '::test1', 'input/large_file',
  1264. '--format', '{num_chunks} {unique_chunks}')
  1265. num_chunks, unique_chunks = map(int, list.split(' '))
  1266. # test1 and test2 do not deduplicate
  1267. assert num_chunks == unique_chunks
  1268. self.cmd('recreate', self.repository_location, '--chunker-params', 'default')
  1269. # test1 and test2 do deduplicate after recreate
  1270. assert not int(self.cmd('list', self.repository_location + '::test1', 'input/large_file',
  1271. '--format', '{unique_chunks}'))
  1272. def test_recreate_recompress(self):
  1273. self.create_regular_file('compressible', size=10000)
  1274. self.cmd('init', self.repository_location)
  1275. self.cmd('create', self.repository_location + '::test', 'input', '-C', 'none')
  1276. file_list = self.cmd('list', self.repository_location + '::test', 'input/compressible',
  1277. '--format', '{size} {csize} {sha256}')
  1278. size, csize, sha256_before = file_list.split(' ')
  1279. assert int(csize) >= int(size) # >= due to metadata overhead
  1280. self.cmd('recreate', self.repository_location, '-C', 'lz4')
  1281. file_list = self.cmd('list', self.repository_location + '::test', 'input/compressible',
  1282. '--format', '{size} {csize} {sha256}')
  1283. size, csize, sha256_after = file_list.split(' ')
  1284. assert int(csize) < int(size)
  1285. assert sha256_before == sha256_after
  1286. def test_recreate_dry_run(self):
  1287. self.create_regular_file('compressible', size=10000)
  1288. self.cmd('init', self.repository_location)
  1289. self.cmd('create', self.repository_location + '::test', 'input')
  1290. archives_before = self.cmd('list', self.repository_location + '::test')
  1291. self.cmd('recreate', self.repository_location, '-n', '-e', 'input/compressible')
  1292. archives_after = self.cmd('list', self.repository_location + '::test')
  1293. assert archives_after == archives_before
  1294. def _recreate_interrupt_patch(self, interrupt_after_n_1_files):
  1295. def interrupt(self, *args):
  1296. if interrupt_after_n_1_files:
  1297. self.interrupt = True
  1298. pi_save(self, *args)
  1299. else:
  1300. raise ArchiveRecreater.Interrupted
  1301. def process_item_patch(*args):
  1302. return pi_call.pop(0)(*args)
  1303. pi_save = ArchiveRecreater.process_item
  1304. pi_call = [pi_save] * interrupt_after_n_1_files + [interrupt]
  1305. return process_item_patch
  1306. def _test_recreate_interrupt(self, change_args, interrupt_early):
  1307. self.create_test_files()
  1308. self.create_regular_file('dir2/abcdef', size=1024 * 80)
  1309. self.cmd('init', self.repository_location)
  1310. self.cmd('create', self.repository_location + '::test', 'input')
  1311. process_files = 1
  1312. if interrupt_early:
  1313. process_files = 0
  1314. with patch.object(ArchiveRecreater, 'process_item', self._recreate_interrupt_patch(process_files)):
  1315. self.cmd('recreate', self.repository_location, 'input/dir2')
  1316. assert 'test.recreate' in self.cmd('list', self.repository_location)
  1317. if change_args:
  1318. with patch.object(sys, 'argv', sys.argv + ['non-forking tests don\'t use sys.argv']):
  1319. output = self.cmd('recreate', '-sv', '--list', '-pC', 'lz4', self.repository_location, 'input/dir2')
  1320. else:
  1321. output = self.cmd('recreate', '-sv', '--list', self.repository_location, 'input/dir2')
  1322. assert 'Found test.recreate, will resume' in output
  1323. assert change_args == ('Command line changed' in output)
  1324. if not interrupt_early:
  1325. assert 'Fast-forwarded to input/dir2/abcdef' in output
  1326. assert 'A input/dir2/abcdef' not in output
  1327. assert 'A input/dir2/file2' in output
  1328. archives = self.cmd('list', self.repository_location)
  1329. assert 'test.recreate' not in archives
  1330. assert 'test' in archives
  1331. files = self.cmd('list', self.repository_location + '::test')
  1332. assert 'dir2/file2' in files
  1333. assert 'dir2/abcdef' in files
  1334. assert 'file1' not in files
  1335. # The _test_create_interrupt requires a deterministic (alphabetic) order of the files to easily check if
  1336. # resumption works correctly. Patch scandir_inorder to work in alphabetic order.
  1337. def test_recreate_interrupt(self):
  1338. with patch.object(helpers, 'scandir_inorder', helpers.scandir_generic):
  1339. self._test_recreate_interrupt(False, True)
  1340. def test_recreate_interrupt2(self):
  1341. with patch.object(helpers, 'scandir_inorder', helpers.scandir_generic):
  1342. self._test_recreate_interrupt(True, False)
  1343. def _test_recreate_chunker_interrupt_patch(self):
  1344. real_add_chunk = Cache.add_chunk
  1345. def add_chunk(*args, **kwargs):
  1346. frame = inspect.stack()[2]
  1347. try:
  1348. caller_self = frame[0].f_locals['self']
  1349. if isinstance(caller_self, ArchiveRecreater):
  1350. caller_self.interrupt = True
  1351. finally:
  1352. del frame
  1353. return real_add_chunk(*args, **kwargs)
  1354. return add_chunk
  1355. def test_recreate_rechunkify_interrupt(self):
  1356. self.create_regular_file('file1', size=1024 * 80)
  1357. self.cmd('init', self.repository_location)
  1358. self.cmd('create', self.repository_location + '::test', 'input')
  1359. archive_before = self.cmd('list', self.repository_location + '::test', '--format', '{sha512}')
  1360. with patch.object(Cache, 'add_chunk', self._test_recreate_chunker_interrupt_patch()):
  1361. self.cmd('recreate', '-pv', '--chunker-params', '10,13,11,4095', self.repository_location)
  1362. assert 'test.recreate' in self.cmd('list', self.repository_location)
  1363. output = self.cmd('recreate', '-svp', '--debug', '--chunker-params', '10,13,11,4095', self.repository_location)
  1364. assert 'Found test.recreate, will resume' in output
  1365. assert 'Copied 1 chunks from a partially processed item' in output
  1366. archive_after = self.cmd('list', self.repository_location + '::test', '--format', '{sha512}')
  1367. assert archive_after == archive_before
  1368. def test_recreate_changed_source(self):
  1369. self.create_test_files()
  1370. self.cmd('init', self.repository_location)
  1371. self.cmd('create', self.repository_location + '::test', 'input')
  1372. with patch.object(ArchiveRecreater, 'process_item', self._recreate_interrupt_patch(1)):
  1373. self.cmd('recreate', self.repository_location, 'input/dir2')
  1374. assert 'test.recreate' in self.cmd('list', self.repository_location)
  1375. self.cmd('delete', self.repository_location + '::test')
  1376. self.cmd('create', self.repository_location + '::test', 'input')
  1377. output = self.cmd('recreate', self.repository_location, 'input/dir2')
  1378. assert 'Source archive changed, will discard test.recreate and start over' in output
  1379. def test_recreate_refuses_temporary(self):
  1380. self.cmd('init', self.repository_location)
  1381. self.cmd('recreate', self.repository_location + '::cba.recreate', exit_code=2)
  1382. def test_recreate_skips_nothing_to_do(self):
  1383. self.create_regular_file('file1', size=1024 * 80)
  1384. self.cmd('init', self.repository_location)
  1385. self.cmd('create', self.repository_location + '::test', 'input')
  1386. info_before = self.cmd('info', self.repository_location + '::test')
  1387. self.cmd('recreate', self.repository_location, '--chunker-params', 'default')
  1388. info_after = self.cmd('info', self.repository_location + '::test')
  1389. assert info_before == info_after # includes archive ID
  1390. def test_with_lock(self):
  1391. self.cmd('init', self.repository_location)
  1392. lock_path = os.path.join(self.repository_path, 'lock.exclusive')
  1393. cmd = 'python3', '-c', 'import os, sys; sys.exit(42 if os.path.exists("%s") else 23)' % lock_path
  1394. self.cmd('with-lock', self.repository_location, *cmd, fork=True, exit_code=42)
  1395. def test_recreate_list_output(self):
  1396. self.cmd('init', self.repository_location)
  1397. self.create_regular_file('file1', size=0)
  1398. self.create_regular_file('file2', size=0)
  1399. self.create_regular_file('file3', size=0)
  1400. self.create_regular_file('file4', size=0)
  1401. self.create_regular_file('file5', size=0)
  1402. self.cmd('create', self.repository_location + '::test', 'input')
  1403. output = self.cmd('recreate', '--list', '--info', self.repository_location + '::test', '-e', 'input/file2')
  1404. self.assert_in("input/file1", output)
  1405. self.assert_in("x input/file2", output)
  1406. output = self.cmd('recreate', '--list', self.repository_location + '::test', '-e', 'input/file3')
  1407. self.assert_in("input/file1", output)
  1408. self.assert_in("x input/file3", output)
  1409. output = self.cmd('recreate', self.repository_location + '::test', '-e', 'input/file4')
  1410. self.assert_not_in("input/file1", output)
  1411. self.assert_not_in("x input/file4", output)
  1412. output = self.cmd('recreate', '--info', self.repository_location + '::test', '-e', 'input/file5')
  1413. self.assert_not_in("input/file1", output)
  1414. self.assert_not_in("x input/file5", output)
  1415. @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
  1416. class ArchiverTestCaseBinary(ArchiverTestCase):
  1417. EXE = 'borg.exe'
  1418. FORK_DEFAULT = True
  1419. @unittest.skip('patches objects')
  1420. def test_init_interrupt(self):
  1421. pass
  1422. @unittest.skip('patches objects')
  1423. def test_recreate_rechunkify_interrupt(self):
  1424. pass
  1425. @unittest.skip('patches objects')
  1426. def test_recreate_interrupt(self):
  1427. pass
  1428. @unittest.skip('patches objects')
  1429. def test_recreate_changed_source(self):
  1430. pass
  1431. class ArchiverCheckTestCase(ArchiverTestCaseBase):
  1432. def setUp(self):
  1433. super().setUp()
  1434. with patch.object(ChunkBuffer, 'BUFFER_SIZE', 10):
  1435. self.cmd('init', self.repository_location)
  1436. self.create_src_archive('archive1')
  1437. self.create_src_archive('archive2')
  1438. def open_archive(self, name):
  1439. repository = Repository(self.repository_path)
  1440. with repository:
  1441. manifest, key = Manifest.load(repository)
  1442. archive = Archive(repository, key, manifest, name)
  1443. return archive, repository
  1444. def test_check_usage(self):
  1445. output = self.cmd('check', '-v', '--progress', self.repository_location, exit_code=0)
  1446. self.assert_in('Starting repository check', output)
  1447. self.assert_in('Starting archive consistency check', output)
  1448. self.assert_in('Checking segments', output)
  1449. # reset logging to new process default to avoid need for fork=True on next check
  1450. logging.getLogger('borg.output.progress').setLevel(logging.NOTSET)
  1451. output = self.cmd('check', '-v', '--repository-only', self.repository_location, exit_code=0)
  1452. self.assert_in('Starting repository check', output)
  1453. self.assert_not_in('Starting archive consistency check', output)
  1454. self.assert_not_in('Checking segments', output)
  1455. output = self.cmd('check', '-v', '--archives-only', self.repository_location, exit_code=0)
  1456. self.assert_not_in('Starting repository check', output)
  1457. self.assert_in('Starting archive consistency check', output)
  1458. output = self.cmd('check', '-v', '--archives-only', '--prefix=archive2', self.repository_location, exit_code=0)
  1459. self.assert_not_in('archive1', output)
  1460. def test_missing_file_chunk(self):
  1461. archive, repository = self.open_archive('archive1')
  1462. with repository:
  1463. for item in archive.iter_items():
  1464. if item[b'path'].endswith('testsuite/archiver.py'):
  1465. repository.delete(item[b'chunks'][-1].id)
  1466. break
  1467. repository.commit()
  1468. self.cmd('check', self.repository_location, exit_code=1)
  1469. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1470. self.cmd('check', self.repository_location, exit_code=0)
  1471. def test_missing_archive_item_chunk(self):
  1472. archive, repository = self.open_archive('archive1')
  1473. with repository:
  1474. repository.delete(archive.metadata[b'items'][-5])
  1475. repository.commit()
  1476. self.cmd('check', self.repository_location, exit_code=1)
  1477. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1478. self.cmd('check', self.repository_location, exit_code=0)
  1479. def test_missing_archive_metadata(self):
  1480. archive, repository = self.open_archive('archive1')
  1481. with repository:
  1482. repository.delete(archive.id)
  1483. repository.commit()
  1484. self.cmd('check', self.repository_location, exit_code=1)
  1485. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1486. self.cmd('check', self.repository_location, exit_code=0)
  1487. def test_missing_manifest(self):
  1488. archive, repository = self.open_archive('archive1')
  1489. with repository:
  1490. repository.delete(Manifest.MANIFEST_ID)
  1491. repository.commit()
  1492. self.cmd('check', self.repository_location, exit_code=1)
  1493. output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0)
  1494. self.assert_in('archive1', output)
  1495. self.assert_in('archive2', output)
  1496. self.cmd('check', self.repository_location, exit_code=0)
  1497. def test_extra_chunks(self):
  1498. self.cmd('check', self.repository_location, exit_code=0)
  1499. with Repository(self.repository_location) as repository:
  1500. repository.put(b'01234567890123456789012345678901', b'xxxx')
  1501. repository.commit()
  1502. self.cmd('check', self.repository_location, exit_code=1)
  1503. self.cmd('check', self.repository_location, exit_code=1)
  1504. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1505. self.cmd('check', self.repository_location, exit_code=0)
  1506. self.cmd('extract', '--dry-run', self.repository_location + '::archive1', exit_code=0)
  1507. def _test_verify_data(self, *init_args):
  1508. shutil.rmtree(self.repository_path)
  1509. self.cmd('init', self.repository_location, *init_args)
  1510. self.create_src_archive('archive1')
  1511. archive, repository = self.open_archive('archive1')
  1512. with repository:
  1513. for item in archive.iter_items():
  1514. if item[b'path'].endswith('testsuite/archiver.py'):
  1515. chunk = item[b'chunks'][-1]
  1516. data = repository.get(chunk.id) + b'1234'
  1517. repository.put(chunk.id, data)
  1518. break
  1519. repository.commit()
  1520. self.cmd('check', self.repository_location, exit_code=0)
  1521. output = self.cmd('check', '--verify-data', self.repository_location, exit_code=1)
  1522. assert bin_to_hex(chunk.id) + ', integrity error' in output
  1523. def test_verify_data(self):
  1524. self._test_verify_data('--encryption', 'repokey')
  1525. def test_verify_data_unencrypted(self):
  1526. self._test_verify_data('--encryption', 'none')
  1527. class RemoteArchiverTestCase(ArchiverTestCase):
  1528. prefix = '__testsuite__:'
  1529. def test_remote_repo_restrict_to_path(self):
  1530. self.cmd('init', self.repository_location)
  1531. path_prefix = os.path.dirname(self.repository_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. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', path_prefix]):
  1535. self.cmd('init', self.repository_location + '_2')
  1536. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]):
  1537. self.cmd('init', self.repository_location + '_3')
  1538. # skip fuse tests here, they deadlock since this change in exec_cmd:
  1539. # -output = subprocess.check_output(borg + args, stderr=None)
  1540. # +output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT)
  1541. # this was introduced because some tests expect stderr contents to show up
  1542. # in "output" also. Also, the non-forking exec_cmd catches both, too.
  1543. @unittest.skip('deadlock issues')
  1544. def test_fuse_mount_repository(self):
  1545. pass
  1546. @unittest.skip('deadlock issues')
  1547. def test_fuse_mount_archive(self):
  1548. pass
  1549. @unittest.skip('only works locally')
  1550. def test_debug_put_get_delete_obj(self):
  1551. pass
  1552. class DiffArchiverTestCase(ArchiverTestCaseBase):
  1553. def test_basic_functionality(self):
  1554. # Initialize test folder
  1555. self.create_test_files()
  1556. self.cmd('init', self.repository_location)
  1557. # Setup files for the first snapshot
  1558. self.create_regular_file('file_unchanged', size=128)
  1559. self.create_regular_file('file_removed', size=256)
  1560. self.create_regular_file('file_removed2', size=512)
  1561. self.create_regular_file('file_replaced', size=1024)
  1562. os.mkdir('input/dir_replaced_with_file')
  1563. os.chmod('input/dir_replaced_with_file', stat.S_IFDIR | 0o755)
  1564. os.mkdir('input/dir_replaced_with_link')
  1565. os.mkdir('input/dir_removed')
  1566. os.symlink('input/dir_replaced_with_file', 'input/link_changed')
  1567. os.symlink('input/file_unchanged', 'input/link_removed')
  1568. os.symlink('input/file_removed2', 'input/link_target_removed')
  1569. os.symlink('input/empty', 'input/link_target_contents_changed')
  1570. os.symlink('input/empty', 'input/link_replaced_by_file')
  1571. os.link('input/empty', 'input/hardlink_contents_changed')
  1572. os.link('input/file_removed', 'input/hardlink_removed')
  1573. os.link('input/file_removed2', 'input/hardlink_target_removed')
  1574. os.link('input/file_replaced', 'input/hardlink_target_replaced')
  1575. # Create the first snapshot
  1576. self.cmd('create', self.repository_location + '::test0', 'input')
  1577. # Setup files for the second snapshot
  1578. self.create_regular_file('file_added', size=2048)
  1579. os.unlink('input/file_removed')
  1580. os.unlink('input/file_removed2')
  1581. os.unlink('input/file_replaced')
  1582. self.create_regular_file('file_replaced', size=4096, contents=b'0')
  1583. os.rmdir('input/dir_replaced_with_file')
  1584. self.create_regular_file('dir_replaced_with_file', size=8192)
  1585. os.chmod('input/dir_replaced_with_file', stat.S_IFREG | 0o755)
  1586. os.mkdir('input/dir_added')
  1587. os.rmdir('input/dir_removed')
  1588. os.rmdir('input/dir_replaced_with_link')
  1589. os.symlink('input/dir_added', 'input/dir_replaced_with_link')
  1590. os.unlink('input/link_changed')
  1591. os.symlink('input/dir_added', 'input/link_changed')
  1592. os.symlink('input/dir_added', 'input/link_added')
  1593. os.unlink('input/link_removed')
  1594. os.unlink('input/link_replaced_by_file')
  1595. self.create_regular_file('link_replaced_by_file', size=16384)
  1596. os.unlink('input/hardlink_removed')
  1597. os.link('input/file_added', 'input/hardlink_added')
  1598. with open('input/empty', 'ab') as fd:
  1599. fd.write(b'appended_data')
  1600. # Create the second snapshot
  1601. self.cmd('create', self.repository_location + '::test1a', 'input')
  1602. self.cmd('create', '--chunker-params', '16,18,17,4095', self.repository_location + '::test1b', 'input')
  1603. def do_asserts(output, archive):
  1604. # File contents changed (deleted and replaced with a new file)
  1605. assert 'B input/file_replaced' in output
  1606. # File unchanged
  1607. assert 'input/file_unchanged' not in output
  1608. # Directory replaced with a regular file
  1609. assert '[drwxr-xr-x -> -rwxr-xr-x] input/dir_replaced_with_file' in output
  1610. # Basic directory cases
  1611. assert 'added directory input/dir_added' in output
  1612. assert 'removed directory input/dir_removed' in output
  1613. # Basic symlink cases
  1614. assert 'changed link input/link_changed' in output
  1615. assert 'added link input/link_added' in output
  1616. assert 'removed link input/link_removed' in output
  1617. # Symlink replacing or being replaced
  1618. assert '] input/dir_replaced_with_link' in output
  1619. assert '] input/link_replaced_by_file' in output
  1620. # Symlink target removed. Should not affect the symlink at all.
  1621. assert 'input/link_target_removed' not in output
  1622. # The inode has two links and the file contents changed. Borg
  1623. # should notice the changes in both links. However, the symlink
  1624. # pointing to the file is not changed.
  1625. assert '0 B input/empty' in output
  1626. assert '0 B input/hardlink_contents_changed' in output
  1627. assert 'input/link_target_contents_changed' not in output
  1628. # Added a new file and a hard link to it. Both links to the same
  1629. # inode should appear as separate files.
  1630. assert 'added 2.05 kB input/file_added' in output
  1631. assert 'added 2.05 kB input/hardlink_added' in output
  1632. # The inode has two links and both of them are deleted. They should
  1633. # appear as two deleted files.
  1634. assert 'removed 256 B input/file_removed' in output
  1635. assert 'removed 256 B input/hardlink_removed' in output
  1636. # Another link (marked previously as the source in borg) to the
  1637. # same inode was removed. This should not change this link at all.
  1638. assert 'input/hardlink_target_removed' not in output
  1639. # Another link (marked previously as the source in borg) to the
  1640. # same inode was replaced with a new regular file. This should not
  1641. # change this link at all.
  1642. assert 'input/hardlink_target_replaced' not in output
  1643. do_asserts(self.cmd('diff', self.repository_location + '::test0', 'test1a'), '1a')
  1644. # We expect exit_code=1 due to the chunker params warning
  1645. do_asserts(self.cmd('diff', self.repository_location + '::test0', 'test1b', exit_code=1), '1b')
  1646. def test_sort_option(self):
  1647. self.cmd('init', self.repository_location)
  1648. self.create_regular_file('a_file_removed', size=8)
  1649. self.create_regular_file('f_file_removed', size=16)
  1650. self.create_regular_file('c_file_changed', size=32)
  1651. self.create_regular_file('e_file_changed', size=64)
  1652. self.cmd('create', self.repository_location + '::test0', 'input')
  1653. os.unlink('input/a_file_removed')
  1654. os.unlink('input/f_file_removed')
  1655. os.unlink('input/c_file_changed')
  1656. os.unlink('input/e_file_changed')
  1657. self.create_regular_file('c_file_changed', size=512)
  1658. self.create_regular_file('e_file_changed', size=1024)
  1659. self.create_regular_file('b_file_added', size=128)
  1660. self.create_regular_file('d_file_added', size=256)
  1661. self.cmd('create', self.repository_location + '::test1', 'input')
  1662. output = self.cmd('diff', '--sort', self.repository_location + '::test0', 'test1')
  1663. expected = [
  1664. 'a_file_removed',
  1665. 'b_file_added',
  1666. 'c_file_changed',
  1667. 'd_file_added',
  1668. 'e_file_changed',
  1669. 'f_file_removed',
  1670. ]
  1671. assert all(x in line for x, line in zip(expected, output.splitlines()))
  1672. def test_get_args():
  1673. archiver = Archiver()
  1674. # everything normal:
  1675. # first param is argv as produced by ssh forced command,
  1676. # second param is like from SSH_ORIGINAL_COMMAND env variable
  1677. args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
  1678. 'borg serve --info --umask=0027')
  1679. assert args.func == archiver.do_serve
  1680. assert args.restrict_to_paths == ['/p1', '/p2']
  1681. assert args.umask == 0o027
  1682. assert args.log_level == 'info'
  1683. # trying to cheat - break out of path restriction
  1684. args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
  1685. 'borg serve --restrict-to-path=/')
  1686. assert args.restrict_to_paths == ['/p1', '/p2']
  1687. # trying to cheat - try to execute different subcommand
  1688. args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
  1689. 'borg init /')
  1690. assert args.func == archiver.do_serve
  1691. def test_compare_chunk_contents():
  1692. def ccc(a, b):
  1693. chunks_a = [Chunk(data) for data in a]
  1694. chunks_b = [Chunk(data) for data in b]
  1695. compare1 = Archiver.compare_chunk_contents(iter(chunks_a), iter(chunks_b))
  1696. compare2 = Archiver.compare_chunk_contents(iter(chunks_b), iter(chunks_a))
  1697. assert compare1 == compare2
  1698. return compare1
  1699. assert ccc([
  1700. b'1234', b'567A', b'bC'
  1701. ], [
  1702. b'1', b'23', b'4567A', b'b', b'C'
  1703. ])
  1704. # one iterator exhausted before the other
  1705. assert not ccc([
  1706. b'12345',
  1707. ], [
  1708. b'1234', b'56'
  1709. ])
  1710. # content mismatch
  1711. assert not ccc([
  1712. b'1234', b'65'
  1713. ], [
  1714. b'1234', b'56'
  1715. ])
  1716. # first is the prefix of second
  1717. assert not ccc([
  1718. b'1234', b'56'
  1719. ], [
  1720. b'1234', b'565'
  1721. ])