archiver.py 116 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416
  1. from binascii import unhexlify, b2a_base64
  2. from configparser import ConfigParser
  3. import errno
  4. import os
  5. import inspect
  6. from io import StringIO
  7. import logging
  8. import random
  9. import socket
  10. import stat
  11. import subprocess
  12. import sys
  13. import shutil
  14. import tempfile
  15. import time
  16. import unittest
  17. from unittest.mock import patch
  18. from hashlib import sha256
  19. import pytest
  20. try:
  21. import llfuse
  22. except ImportError:
  23. pass
  24. from .. import xattr, helpers, platform
  25. from ..archive import Archive, ChunkBuffer, ArchiveRecreater, flags_noatime, flags_normal
  26. from ..archiver import Archiver
  27. from ..cache import Cache
  28. from ..constants import * # NOQA
  29. from ..crypto import bytes_to_long, num_aes_blocks
  30. from ..helpers import PatternMatcher, parse_pattern, Location
  31. from ..helpers import Chunk, Manifest
  32. from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR
  33. from ..helpers import bin_to_hex
  34. from ..item import Item
  35. from ..key import KeyfileKeyBase, RepoKey, KeyfileKey, Passphrase
  36. from ..keymanager import RepoIdMismatch, NotABorgKeyFile
  37. from ..remote import RemoteRepository, PathNotAllowed
  38. from ..repository import Repository
  39. from . import has_lchflags, has_llfuse
  40. from . import BaseTestCase, changedir, environment_variable, no_selinux
  41. from . import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, is_utime_fully_supported
  42. from .platform import fakeroot_detected
  43. src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
  44. def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw):
  45. if fork:
  46. try:
  47. if exe is None:
  48. borg = (sys.executable, '-m', 'borg.archiver')
  49. elif isinstance(exe, str):
  50. borg = (exe, )
  51. elif not isinstance(exe, tuple):
  52. raise ValueError('exe must be None, a tuple or a str')
  53. output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT)
  54. ret = 0
  55. except subprocess.CalledProcessError as e:
  56. output = e.output
  57. ret = e.returncode
  58. except SystemExit as e: # possibly raised by argparse
  59. output = ''
  60. ret = e.code
  61. return ret, os.fsdecode(output)
  62. else:
  63. stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr
  64. try:
  65. sys.stdin = StringIO()
  66. sys.stdout = sys.stderr = output = StringIO()
  67. if archiver is None:
  68. archiver = Archiver()
  69. archiver.prerun_checks = lambda *args: None
  70. archiver.exit_code = EXIT_SUCCESS
  71. args = archiver.parse_args(list(args))
  72. ret = archiver.run(args)
  73. return ret, output.getvalue()
  74. finally:
  75. sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr
  76. # check if the binary "borg.exe" is available (for local testing a symlink to virtualenv/bin/borg should do)
  77. try:
  78. exec_cmd('help', exe='borg.exe', fork=True)
  79. BORG_EXES = ['python', 'binary', ]
  80. except FileNotFoundError:
  81. BORG_EXES = ['python', ]
  82. @pytest.fixture(params=BORG_EXES)
  83. def cmd(request):
  84. if request.param == 'python':
  85. exe = None
  86. elif request.param == 'binary':
  87. exe = 'borg.exe'
  88. else:
  89. raise ValueError("param must be 'python' or 'binary'")
  90. def exec_fn(*args, **kw):
  91. return exec_cmd(*args, exe=exe, fork=True, **kw)
  92. return exec_fn
  93. def test_return_codes(cmd, tmpdir):
  94. repo = tmpdir.mkdir('repo')
  95. input = tmpdir.mkdir('input')
  96. output = tmpdir.mkdir('output')
  97. input.join('test_file').write('content')
  98. rc, out = cmd('init', '--encryption=none', '%s' % str(repo))
  99. assert rc == EXIT_SUCCESS
  100. rc, out = cmd('create', '%s::archive' % repo, str(input))
  101. assert rc == EXIT_SUCCESS
  102. with changedir(str(output)):
  103. rc, out = cmd('extract', '%s::archive' % repo)
  104. assert rc == EXIT_SUCCESS
  105. rc, out = cmd('extract', '%s::archive' % repo, 'does/not/match')
  106. assert rc == EXIT_WARNING # pattern did not match
  107. rc, out = cmd('create', '%s::archive' % repo, str(input))
  108. assert rc == EXIT_ERROR # duplicate archive name
  109. """
  110. test_disk_full is very slow and not recommended to be included in daily testing.
  111. for this test, an empty, writable 16MB filesystem mounted on DF_MOUNT is required.
  112. for speed and other reasons, it is recommended that the underlying block device is
  113. in RAM, not a magnetic or flash disk.
  114. assuming /tmp is a tmpfs (in memory filesystem), one can use this:
  115. dd if=/dev/zero of=/tmp/borg-disk bs=16M count=1
  116. mkfs.ext4 /tmp/borg-disk
  117. mkdir /tmp/borg-mount
  118. sudo mount /tmp/borg-disk /tmp/borg-mount
  119. if the directory does not exist, the test will be skipped.
  120. """
  121. DF_MOUNT = '/tmp/borg-mount'
  122. @pytest.mark.skipif(not os.path.exists(DF_MOUNT), reason="needs a 16MB fs mounted on %s" % DF_MOUNT)
  123. def test_disk_full(cmd):
  124. def make_files(dir, count, size, rnd=True):
  125. shutil.rmtree(dir, ignore_errors=True)
  126. os.mkdir(dir)
  127. if rnd:
  128. count = random.randint(1, count)
  129. if size > 1:
  130. size = random.randint(1, size)
  131. for i in range(count):
  132. fn = os.path.join(dir, "file%03d" % i)
  133. with open(fn, 'wb') as f:
  134. data = os.urandom(size)
  135. f.write(data)
  136. with environment_variable(BORG_CHECK_I_KNOW_WHAT_I_AM_DOING='YES'):
  137. mount = DF_MOUNT
  138. assert os.path.exists(mount)
  139. repo = os.path.join(mount, 'repo')
  140. input = os.path.join(mount, 'input')
  141. reserve = os.path.join(mount, 'reserve')
  142. for j in range(100):
  143. shutil.rmtree(repo, ignore_errors=True)
  144. shutil.rmtree(input, ignore_errors=True)
  145. # keep some space and some inodes in reserve that we can free up later:
  146. make_files(reserve, 80, 100000, rnd=False)
  147. rc, out = cmd('init', repo)
  148. if rc != EXIT_SUCCESS:
  149. print('init', rc, out)
  150. assert rc == EXIT_SUCCESS
  151. try:
  152. success, i = True, 0
  153. while success:
  154. i += 1
  155. try:
  156. make_files(input, 20, 200000)
  157. except OSError as err:
  158. if err.errno == errno.ENOSPC:
  159. # already out of space
  160. break
  161. raise
  162. try:
  163. rc, out = cmd('create', '%s::test%03d' % (repo, i), input)
  164. success = rc == EXIT_SUCCESS
  165. if not success:
  166. print('create', rc, out)
  167. finally:
  168. # make sure repo is not locked
  169. shutil.rmtree(os.path.join(repo, 'lock.exclusive'), ignore_errors=True)
  170. os.remove(os.path.join(repo, 'lock.roster'))
  171. finally:
  172. # now some error happened, likely we are out of disk space.
  173. # free some space so we can expect borg to be able to work normally:
  174. shutil.rmtree(reserve, ignore_errors=True)
  175. rc, out = cmd('list', repo)
  176. if rc != EXIT_SUCCESS:
  177. print('list', rc, out)
  178. rc, out = cmd('check', '--repair', repo)
  179. if rc != EXIT_SUCCESS:
  180. print('check', rc, out)
  181. assert rc == EXIT_SUCCESS
  182. class ArchiverTestCaseBase(BaseTestCase):
  183. EXE = None # python source based
  184. FORK_DEFAULT = False
  185. prefix = ''
  186. def setUp(self):
  187. os.environ['BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
  188. os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
  189. os.environ['BORG_RECREATE_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
  190. os.environ['BORG_PASSPHRASE'] = 'waytooeasyonlyfortests'
  191. self.archiver = not self.FORK_DEFAULT and Archiver() or None
  192. self.tmpdir = tempfile.mkdtemp()
  193. self.repository_path = os.path.join(self.tmpdir, 'repository')
  194. self.repository_location = self.prefix + self.repository_path
  195. self.input_path = os.path.join(self.tmpdir, 'input')
  196. self.output_path = os.path.join(self.tmpdir, 'output')
  197. self.keys_path = os.path.join(self.tmpdir, 'keys')
  198. self.cache_path = os.path.join(self.tmpdir, 'cache')
  199. self.exclude_file_path = os.path.join(self.tmpdir, 'excludes')
  200. os.environ['BORG_KEYS_DIR'] = self.keys_path
  201. os.environ['BORG_CACHE_DIR'] = self.cache_path
  202. os.mkdir(self.input_path)
  203. os.chmod(self.input_path, 0o777) # avoid troubles with fakeroot / FUSE
  204. os.mkdir(self.output_path)
  205. os.mkdir(self.keys_path)
  206. os.mkdir(self.cache_path)
  207. with open(self.exclude_file_path, 'wb') as fd:
  208. fd.write(b'input/file2\n# A comment line, then a blank line\n\n')
  209. self._old_wd = os.getcwd()
  210. os.chdir(self.tmpdir)
  211. def tearDown(self):
  212. os.chdir(self._old_wd)
  213. # note: ignore_errors=True as workaround for issue #862
  214. shutil.rmtree(self.tmpdir, ignore_errors=True)
  215. # destroy logging configuration
  216. logging.Logger.manager.loggerDict.clear()
  217. def cmd(self, *args, **kw):
  218. exit_code = kw.pop('exit_code', 0)
  219. fork = kw.pop('fork', None)
  220. if fork is None:
  221. fork = self.FORK_DEFAULT
  222. ret, output = exec_cmd(*args, fork=fork, exe=self.EXE, archiver=self.archiver, **kw)
  223. if ret != exit_code:
  224. print(output)
  225. self.assert_equal(ret, exit_code)
  226. return output
  227. def create_src_archive(self, name):
  228. self.cmd('create', self.repository_location + '::' + name, src_dir)
  229. def open_archive(self, name):
  230. repository = Repository(self.repository_path, exclusive=True)
  231. with repository:
  232. manifest, key = Manifest.load(repository)
  233. archive = Archive(repository, key, manifest, name)
  234. return archive, repository
  235. def open_repository(self):
  236. return Repository(self.repository_path, exclusive=True)
  237. def create_regular_file(self, name, size=0, contents=None):
  238. filename = os.path.join(self.input_path, name)
  239. if not os.path.exists(os.path.dirname(filename)):
  240. os.makedirs(os.path.dirname(filename))
  241. with open(filename, 'wb') as fd:
  242. if contents is None:
  243. contents = b'X' * size
  244. fd.write(contents)
  245. def create_test_files(self):
  246. """Create a minimal test case including all supported file types
  247. """
  248. # File
  249. self.create_regular_file('empty', size=0)
  250. # next code line raises OverflowError on 32bit cpu (raspberry pi 2):
  251. # 2600-01-01 > 2**64 ns
  252. # os.utime('input/empty', (19880895600, 19880895600))
  253. # thus, we better test with something not that far in future:
  254. # 2038-01-19 (1970 + 2^31 - 1 seconds) is the 32bit "deadline":
  255. os.utime('input/empty', (2**31 - 1, 2**31 - 1))
  256. self.create_regular_file('file1', size=1024 * 80)
  257. self.create_regular_file('flagfile', size=1024)
  258. # Directory
  259. self.create_regular_file('dir2/file2', size=1024 * 80)
  260. # File mode
  261. os.chmod('input/file1', 0o4755)
  262. # Hard link
  263. if are_hardlinks_supported():
  264. os.link(os.path.join(self.input_path, 'file1'),
  265. os.path.join(self.input_path, 'hardlink'))
  266. # Symlink
  267. if are_symlinks_supported():
  268. os.symlink('somewhere', os.path.join(self.input_path, 'link1'))
  269. self.create_regular_file('fusexattr', size=1)
  270. if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path):
  271. # ironically, due to the way how fakeroot works, comparing fuse file xattrs to orig file xattrs
  272. # will FAIL if fakeroot supports xattrs, thus we only set the xattr if XATTR_FAKEROOT is False.
  273. # This is because fakeroot with xattr-support does not propagate xattrs of the underlying file
  274. # into "fakeroot space". Because the xattrs exposed by borgfs are these of an underlying file
  275. # (from fakeroots point of view) they are invisible to the test process inside the fakeroot.
  276. xattr.setxattr(os.path.join(self.input_path, 'fusexattr'), 'user.foo', b'bar')
  277. # XXX this always fails for me
  278. # ubuntu 14.04, on a TMP dir filesystem with user_xattr, using fakeroot
  279. # same for newer ubuntu and centos.
  280. # if this is supported just on specific platform, platform should be checked first,
  281. # so that the test setup for all tests using it does not fail here always for others.
  282. # xattr.setxattr(os.path.join(self.input_path, 'link1'), 'user.foo_symlink', b'bar_symlink', follow_symlinks=False)
  283. # FIFO node
  284. if are_fifos_supported():
  285. os.mkfifo(os.path.join(self.input_path, 'fifo1'))
  286. if has_lchflags:
  287. platform.set_flags(os.path.join(self.input_path, 'flagfile'), stat.UF_NODUMP)
  288. try:
  289. # Block device
  290. os.mknod('input/bdev', 0o600 | stat.S_IFBLK, os.makedev(10, 20))
  291. # Char device
  292. os.mknod('input/cdev', 0o600 | stat.S_IFCHR, os.makedev(30, 40))
  293. # File mode
  294. os.chmod('input/dir2', 0o555) # if we take away write perms, we need root to remove contents
  295. # File owner
  296. os.chown('input/file1', 100, 200) # raises OSError invalid argument on cygwin
  297. have_root = True # we have (fake)root
  298. except PermissionError:
  299. have_root = False
  300. except OSError as e:
  301. if e.errno != errno.EINVAL:
  302. raise
  303. have_root = False
  304. return have_root
  305. class ArchiverTestCase(ArchiverTestCaseBase):
  306. def test_basic_functionality(self):
  307. have_root = self.create_test_files()
  308. # fork required to test show-rc output
  309. output = self.cmd('init', '--show-version', '--show-rc', self.repository_location, fork=True)
  310. self.assert_in('borgbackup version', output)
  311. self.assert_in('terminating with success status, rc 0', output)
  312. self.cmd('create', self.repository_location + '::test', 'input')
  313. output = self.cmd('create', '--stats', self.repository_location + '::test.2', 'input')
  314. self.assert_in('Archive name: test.2', output)
  315. self.assert_in('This archive: ', output)
  316. with changedir('output'):
  317. self.cmd('extract', self.repository_location + '::test')
  318. list_output = self.cmd('list', '--short', self.repository_location)
  319. self.assert_in('test', list_output)
  320. self.assert_in('test.2', list_output)
  321. expected = [
  322. 'input',
  323. 'input/bdev',
  324. 'input/cdev',
  325. 'input/dir2',
  326. 'input/dir2/file2',
  327. 'input/empty',
  328. 'input/file1',
  329. 'input/flagfile',
  330. ]
  331. if are_fifos_supported():
  332. expected.append('input/fifo1')
  333. if are_symlinks_supported():
  334. expected.append('input/link1')
  335. if are_hardlinks_supported():
  336. expected.append('input/hardlink')
  337. if not have_root:
  338. # we could not create these device files without (fake)root
  339. expected.remove('input/bdev')
  340. expected.remove('input/cdev')
  341. if has_lchflags:
  342. # remove the file we did not backup, so input and output become equal
  343. expected.remove('input/flagfile') # this file is UF_NODUMP
  344. os.remove(os.path.join('input', 'flagfile'))
  345. list_output = self.cmd('list', '--short', self.repository_location + '::test')
  346. for name in expected:
  347. self.assert_in(name, list_output)
  348. self.assert_dirs_equal('input', 'output/input')
  349. info_output = self.cmd('info', self.repository_location + '::test')
  350. item_count = 4 if has_lchflags else 5 # one file is UF_NODUMP
  351. self.assert_in('Number of files: %d' % item_count, info_output)
  352. shutil.rmtree(self.cache_path)
  353. with environment_variable(BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='yes'):
  354. info_output2 = self.cmd('info', self.repository_location + '::test')
  355. def filter(output):
  356. # filter for interesting "info" output, ignore cache rebuilding related stuff
  357. prefixes = ['Name:', 'Fingerprint:', 'Number of files:', 'This archive:',
  358. 'All archives:', 'Chunk index:', ]
  359. result = []
  360. for line in output.splitlines():
  361. for prefix in prefixes:
  362. if line.startswith(prefix):
  363. result.append(line)
  364. return '\n'.join(result)
  365. # the interesting parts of info_output2 and info_output should be same
  366. self.assert_equal(filter(info_output), filter(info_output2))
  367. def test_unix_socket(self):
  368. self.cmd('init', self.repository_location)
  369. try:
  370. sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  371. sock.bind(os.path.join(self.input_path, 'unix-socket'))
  372. except PermissionError as err:
  373. if err.errno == errno.EPERM:
  374. pytest.skip('unix sockets disabled or not supported')
  375. elif err.errno == errno.EACCES:
  376. pytest.skip('permission denied to create unix sockets')
  377. self.cmd('create', self.repository_location + '::test', 'input')
  378. sock.close()
  379. with changedir('output'):
  380. self.cmd('extract', self.repository_location + '::test')
  381. assert not os.path.exists('input/unix-socket')
  382. @pytest.mark.skipif(not are_symlinks_supported(), reason='symlinks not supported')
  383. def test_symlink_extract(self):
  384. self.create_test_files()
  385. self.cmd('init', self.repository_location)
  386. self.cmd('create', self.repository_location + '::test', 'input')
  387. with changedir('output'):
  388. self.cmd('extract', self.repository_location + '::test')
  389. assert os.readlink('input/link1') == 'somewhere'
  390. # Search for O_NOATIME there: https://www.gnu.org/software/hurd/contributing.html - we just
  391. # skip the test on Hurd, it is not critical anyway, just testing a performance optimization.
  392. @pytest.mark.skipif(sys.platform == 'gnu0', reason="O_NOATIME is strangely broken on GNU Hurd")
  393. @pytest.mark.skipif(not is_utime_fully_supported(), reason='cannot properly setup and execute test without utime')
  394. def test_atime(self):
  395. def has_noatime(some_file):
  396. atime_before = os.stat(some_file).st_atime_ns
  397. try:
  398. with open(os.open(some_file, flags_noatime)) as file:
  399. file.read()
  400. except PermissionError:
  401. return False
  402. else:
  403. atime_after = os.stat(some_file).st_atime_ns
  404. noatime_used = flags_noatime != flags_normal
  405. return noatime_used and atime_before == atime_after
  406. self.create_test_files()
  407. atime, mtime = 123456780, 234567890
  408. have_noatime = has_noatime('input/file1')
  409. os.utime('input/file1', (atime, mtime))
  410. self.cmd('init', self.repository_location)
  411. self.cmd('create', self.repository_location + '::test', 'input')
  412. with changedir('output'):
  413. self.cmd('extract', self.repository_location + '::test')
  414. sti = os.stat('input/file1')
  415. sto = os.stat('output/input/file1')
  416. assert sti.st_mtime_ns == sto.st_mtime_ns == mtime * 1e9
  417. if have_noatime:
  418. assert sti.st_atime_ns == sto.st_atime_ns == atime * 1e9
  419. else:
  420. # it touched the input file's atime while backing it up
  421. assert sto.st_atime_ns == atime * 1e9
  422. def _extract_repository_id(self, path):
  423. with Repository(self.repository_path) as repository:
  424. return repository.id
  425. def _set_repository_id(self, path, id):
  426. config = ConfigParser(interpolation=None)
  427. config.read(os.path.join(path, 'config'))
  428. config.set('repository', 'id', bin_to_hex(id))
  429. with open(os.path.join(path, 'config'), 'w') as fd:
  430. config.write(fd)
  431. with Repository(self.repository_path) as repository:
  432. return repository.id
  433. def test_sparse_file(self):
  434. def is_sparse(fn, total_size, hole_size):
  435. st = os.stat(fn)
  436. assert st.st_size == total_size
  437. sparse = True
  438. if sparse and hasattr(st, 'st_blocks') and st.st_blocks * 512 >= st.st_size:
  439. sparse = False
  440. if sparse and hasattr(os, 'SEEK_HOLE') and hasattr(os, 'SEEK_DATA'):
  441. with open(fn, 'rb') as fd:
  442. # only check if the first hole is as expected, because the 2nd hole check
  443. # is problematic on xfs due to its "dynamic speculative EOF preallocation
  444. try:
  445. if fd.seek(0, os.SEEK_HOLE) != 0:
  446. sparse = False
  447. if fd.seek(0, os.SEEK_DATA) != hole_size:
  448. sparse = False
  449. except OSError:
  450. # OS/FS does not really support SEEK_HOLE/SEEK_DATA
  451. sparse = False
  452. return sparse
  453. filename = os.path.join(self.input_path, 'sparse')
  454. content = b'foobar'
  455. hole_size = 5 * (1 << CHUNK_MAX_EXP) # 5 full chunker buffers
  456. total_size = hole_size + len(content) + hole_size
  457. with open(filename, 'wb') as fd:
  458. # create a file that has a hole at the beginning and end (if the
  459. # OS and filesystem supports sparse files)
  460. fd.seek(hole_size, 1)
  461. fd.write(content)
  462. fd.seek(hole_size, 1)
  463. pos = fd.tell()
  464. fd.truncate(pos)
  465. # we first check if we could create a sparse input file:
  466. sparse_support = is_sparse(filename, total_size, hole_size)
  467. if sparse_support:
  468. # we could create a sparse input file, so creating a backup of it and
  469. # extracting it again (as sparse) should also work:
  470. self.cmd('init', self.repository_location)
  471. self.cmd('create', self.repository_location + '::test', 'input')
  472. with changedir(self.output_path):
  473. self.cmd('extract', '--sparse', self.repository_location + '::test')
  474. self.assert_dirs_equal('input', 'output/input')
  475. filename = os.path.join(self.output_path, 'input', 'sparse')
  476. with open(filename, 'rb') as fd:
  477. # check if file contents are as expected
  478. self.assert_equal(fd.read(hole_size), b'\0' * hole_size)
  479. self.assert_equal(fd.read(len(content)), content)
  480. self.assert_equal(fd.read(hole_size), b'\0' * hole_size)
  481. self.assert_true(is_sparse(filename, total_size, hole_size))
  482. def test_unusual_filenames(self):
  483. filenames = ['normal', 'with some blanks', '(with_parens)', ]
  484. for filename in filenames:
  485. filename = os.path.join(self.input_path, filename)
  486. with open(filename, 'wb'):
  487. pass
  488. self.cmd('init', self.repository_location)
  489. self.cmd('create', self.repository_location + '::test', 'input')
  490. for filename in filenames:
  491. with changedir('output'):
  492. self.cmd('extract', self.repository_location + '::test', os.path.join('input', filename))
  493. assert os.path.exists(os.path.join('output', 'input', filename))
  494. def test_repository_swap_detection(self):
  495. self.create_test_files()
  496. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  497. self.cmd('init', '--encryption=repokey', self.repository_location)
  498. repository_id = self._extract_repository_id(self.repository_path)
  499. self.cmd('create', self.repository_location + '::test', 'input')
  500. shutil.rmtree(self.repository_path)
  501. self.cmd('init', '--encryption=none', self.repository_location)
  502. self._set_repository_id(self.repository_path, repository_id)
  503. self.assert_equal(repository_id, self._extract_repository_id(self.repository_path))
  504. if self.FORK_DEFAULT:
  505. self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=EXIT_ERROR)
  506. else:
  507. self.assert_raises(Cache.EncryptionMethodMismatch, lambda: self.cmd('create', self.repository_location + '::test.2', 'input'))
  508. def test_repository_swap_detection2(self):
  509. self.create_test_files()
  510. self.cmd('init', '--encryption=none', self.repository_location + '_unencrypted')
  511. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  512. self.cmd('init', '--encryption=repokey', self.repository_location + '_encrypted')
  513. self.cmd('create', self.repository_location + '_encrypted::test', 'input')
  514. shutil.rmtree(self.repository_path + '_encrypted')
  515. os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted')
  516. if self.FORK_DEFAULT:
  517. self.cmd('create', self.repository_location + '_encrypted::test.2', 'input', exit_code=EXIT_ERROR)
  518. else:
  519. self.assert_raises(Cache.RepositoryAccessAborted, lambda: self.cmd('create', self.repository_location + '_encrypted::test.2', 'input'))
  520. def test_strip_components(self):
  521. self.cmd('init', self.repository_location)
  522. self.create_regular_file('dir/file')
  523. self.cmd('create', self.repository_location + '::test', 'input')
  524. with changedir('output'):
  525. self.cmd('extract', self.repository_location + '::test', '--strip-components', '3')
  526. self.assert_true(not os.path.exists('file'))
  527. with self.assert_creates_file('file'):
  528. self.cmd('extract', self.repository_location + '::test', '--strip-components', '2')
  529. with self.assert_creates_file('dir/file'):
  530. self.cmd('extract', self.repository_location + '::test', '--strip-components', '1')
  531. with self.assert_creates_file('input/dir/file'):
  532. self.cmd('extract', self.repository_location + '::test', '--strip-components', '0')
  533. def _extract_hardlinks_setup(self):
  534. os.mkdir(os.path.join(self.input_path, 'dir1'))
  535. os.mkdir(os.path.join(self.input_path, 'dir1/subdir'))
  536. self.create_regular_file('source')
  537. os.link(os.path.join(self.input_path, 'source'),
  538. os.path.join(self.input_path, 'abba'))
  539. os.link(os.path.join(self.input_path, 'source'),
  540. os.path.join(self.input_path, 'dir1/hardlink'))
  541. os.link(os.path.join(self.input_path, 'source'),
  542. os.path.join(self.input_path, 'dir1/subdir/hardlink'))
  543. self.create_regular_file('dir1/source2')
  544. os.link(os.path.join(self.input_path, 'dir1/source2'),
  545. os.path.join(self.input_path, 'dir1/aaaa'))
  546. self.cmd('init', self.repository_location)
  547. self.cmd('create', self.repository_location + '::test', 'input')
  548. @pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported')
  549. def test_strip_components_links(self):
  550. self._extract_hardlinks_setup()
  551. with changedir('output'):
  552. self.cmd('extract', self.repository_location + '::test', '--strip-components', '2')
  553. assert os.stat('hardlink').st_nlink == 2
  554. assert os.stat('subdir/hardlink').st_nlink == 2
  555. assert os.stat('aaaa').st_nlink == 2
  556. assert os.stat('source2').st_nlink == 2
  557. with changedir('output'):
  558. self.cmd('extract', self.repository_location + '::test')
  559. assert os.stat('input/dir1/hardlink').st_nlink == 4
  560. @pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported')
  561. def test_extract_hardlinks(self):
  562. self._extract_hardlinks_setup()
  563. with changedir('output'):
  564. self.cmd('extract', self.repository_location + '::test', 'input/dir1')
  565. assert os.stat('input/dir1/hardlink').st_nlink == 2
  566. assert os.stat('input/dir1/subdir/hardlink').st_nlink == 2
  567. assert os.stat('input/dir1/aaaa').st_nlink == 2
  568. assert os.stat('input/dir1/source2').st_nlink == 2
  569. with changedir('output'):
  570. self.cmd('extract', self.repository_location + '::test')
  571. assert os.stat('input/dir1/hardlink').st_nlink == 4
  572. def test_extract_include_exclude(self):
  573. self.cmd('init', self.repository_location)
  574. self.create_regular_file('file1', size=1024 * 80)
  575. self.create_regular_file('file2', size=1024 * 80)
  576. self.create_regular_file('file3', size=1024 * 80)
  577. self.create_regular_file('file4', size=1024 * 80)
  578. self.cmd('create', '--exclude=input/file4', self.repository_location + '::test', 'input')
  579. with changedir('output'):
  580. self.cmd('extract', self.repository_location + '::test', 'input/file1', )
  581. self.assert_equal(sorted(os.listdir('output/input')), ['file1'])
  582. with changedir('output'):
  583. self.cmd('extract', '--exclude=input/file2', self.repository_location + '::test')
  584. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  585. with changedir('output'):
  586. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  587. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  588. def test_extract_include_exclude_regex(self):
  589. self.cmd('init', self.repository_location)
  590. self.create_regular_file('file1', size=1024 * 80)
  591. self.create_regular_file('file2', size=1024 * 80)
  592. self.create_regular_file('file3', size=1024 * 80)
  593. self.create_regular_file('file4', size=1024 * 80)
  594. self.create_regular_file('file333', size=1024 * 80)
  595. # Create with regular expression exclusion for file4
  596. self.cmd('create', '--exclude=re:input/file4$', self.repository_location + '::test', 'input')
  597. with changedir('output'):
  598. self.cmd('extract', self.repository_location + '::test')
  599. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333'])
  600. shutil.rmtree('output/input')
  601. # Extract with regular expression exclusion
  602. with changedir('output'):
  603. self.cmd('extract', '--exclude=re:file3+', self.repository_location + '::test')
  604. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2'])
  605. shutil.rmtree('output/input')
  606. # Combine --exclude with fnmatch and regular expression
  607. with changedir('output'):
  608. self.cmd('extract', '--exclude=input/file2', '--exclude=re:file[01]', self.repository_location + '::test')
  609. self.assert_equal(sorted(os.listdir('output/input')), ['file3', 'file333'])
  610. shutil.rmtree('output/input')
  611. # Combine --exclude-from and regular expression exclusion
  612. with changedir('output'):
  613. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, '--exclude=re:file1',
  614. '--exclude=re:file(\\d)\\1\\1$', self.repository_location + '::test')
  615. self.assert_equal(sorted(os.listdir('output/input')), ['file3'])
  616. def test_extract_include_exclude_regex_from_file(self):
  617. self.cmd('init', self.repository_location)
  618. self.create_regular_file('file1', size=1024 * 80)
  619. self.create_regular_file('file2', size=1024 * 80)
  620. self.create_regular_file('file3', size=1024 * 80)
  621. self.create_regular_file('file4', size=1024 * 80)
  622. self.create_regular_file('file333', size=1024 * 80)
  623. self.create_regular_file('aa:something', size=1024 * 80)
  624. # Create while excluding using mixed pattern styles
  625. with open(self.exclude_file_path, 'wb') as fd:
  626. fd.write(b're:input/file4$\n')
  627. fd.write(b'fm:*aa:*thing\n')
  628. self.cmd('create', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test', 'input')
  629. with changedir('output'):
  630. self.cmd('extract', self.repository_location + '::test')
  631. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2', 'file3', 'file333'])
  632. shutil.rmtree('output/input')
  633. # Exclude using regular expression
  634. with open(self.exclude_file_path, 'wb') as fd:
  635. fd.write(b're:file3+\n')
  636. with changedir('output'):
  637. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  638. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file2'])
  639. shutil.rmtree('output/input')
  640. # Mixed exclude pattern styles
  641. with open(self.exclude_file_path, 'wb') as fd:
  642. fd.write(b're:file(\\d)\\1\\1$\n')
  643. fd.write(b'fm:nothingwillmatchthis\n')
  644. fd.write(b'*/file1\n')
  645. fd.write(b're:file2$\n')
  646. with changedir('output'):
  647. self.cmd('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  648. self.assert_equal(sorted(os.listdir('output/input')), ['file3'])
  649. def test_extract_with_pattern(self):
  650. self.cmd("init", self.repository_location)
  651. self.create_regular_file("file1", size=1024 * 80)
  652. self.create_regular_file("file2", size=1024 * 80)
  653. self.create_regular_file("file3", size=1024 * 80)
  654. self.create_regular_file("file4", size=1024 * 80)
  655. self.create_regular_file("file333", size=1024 * 80)
  656. self.cmd("create", self.repository_location + "::test", "input")
  657. # Extract everything with regular expression
  658. with changedir("output"):
  659. self.cmd("extract", self.repository_location + "::test", "re:.*")
  660. self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file3", "file333", "file4"])
  661. shutil.rmtree("output/input")
  662. # Extract with pattern while also excluding files
  663. with changedir("output"):
  664. self.cmd("extract", "--exclude=re:file[34]$", self.repository_location + "::test", r"re:file\d$")
  665. self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2"])
  666. shutil.rmtree("output/input")
  667. # Combine --exclude with pattern for extraction
  668. with changedir("output"):
  669. self.cmd("extract", "--exclude=input/file1", self.repository_location + "::test", "re:file[12]$")
  670. self.assert_equal(sorted(os.listdir("output/input")), ["file2"])
  671. shutil.rmtree("output/input")
  672. # Multiple pattern
  673. with changedir("output"):
  674. self.cmd("extract", self.repository_location + "::test", "fm:input/file1", "fm:*file33*", "input/file2")
  675. self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file333"])
  676. def test_extract_list_output(self):
  677. self.cmd('init', self.repository_location)
  678. self.create_regular_file('file', size=1024 * 80)
  679. self.cmd('create', self.repository_location + '::test', 'input')
  680. with changedir('output'):
  681. output = self.cmd('extract', self.repository_location + '::test')
  682. self.assert_not_in("input/file", output)
  683. shutil.rmtree('output/input')
  684. with changedir('output'):
  685. output = self.cmd('extract', '--info', self.repository_location + '::test')
  686. self.assert_not_in("input/file", output)
  687. shutil.rmtree('output/input')
  688. with changedir('output'):
  689. output = self.cmd('extract', '--list', self.repository_location + '::test')
  690. self.assert_in("input/file", output)
  691. shutil.rmtree('output/input')
  692. with changedir('output'):
  693. output = self.cmd('extract', '--list', '--info', self.repository_location + '::test')
  694. self.assert_in("input/file", output)
  695. def test_extract_progress(self):
  696. self.cmd('init', self.repository_location)
  697. self.create_regular_file('file', size=1024 * 80)
  698. self.cmd('create', self.repository_location + '::test', 'input')
  699. with changedir('output'):
  700. output = self.cmd('extract', self.repository_location + '::test', '--progress')
  701. assert 'Extracting:' in output
  702. def _create_test_caches(self):
  703. self.cmd('init', self.repository_location)
  704. self.create_regular_file('file1', size=1024 * 80)
  705. self.create_regular_file('cache1/%s' % CACHE_TAG_NAME,
  706. contents=CACHE_TAG_CONTENTS + b' extra stuff')
  707. self.create_regular_file('cache2/%s' % CACHE_TAG_NAME,
  708. contents=b'invalid signature')
  709. os.mkdir('input/cache3')
  710. os.link('input/cache1/%s' % CACHE_TAG_NAME, 'input/cache3/%s' % CACHE_TAG_NAME)
  711. def _assert_test_caches(self):
  712. with changedir('output'):
  713. self.cmd('extract', self.repository_location + '::test')
  714. self.assert_equal(sorted(os.listdir('output/input')), ['cache2', 'file1'])
  715. self.assert_equal(sorted(os.listdir('output/input/cache2')), [CACHE_TAG_NAME])
  716. def test_exclude_caches(self):
  717. self._create_test_caches()
  718. self.cmd('create', '--exclude-caches', self.repository_location + '::test', 'input')
  719. self._assert_test_caches()
  720. def test_recreate_exclude_caches(self):
  721. self._create_test_caches()
  722. self.cmd('create', self.repository_location + '::test', 'input')
  723. self.cmd('recreate', '--exclude-caches', self.repository_location + '::test')
  724. self._assert_test_caches()
  725. def _create_test_tagged(self):
  726. self.cmd('init', self.repository_location)
  727. self.create_regular_file('file1', size=1024 * 80)
  728. self.create_regular_file('tagged1/.NOBACKUP')
  729. self.create_regular_file('tagged2/00-NOBACKUP')
  730. self.create_regular_file('tagged3/.NOBACKUP/file2')
  731. def _assert_test_tagged(self):
  732. with changedir('output'):
  733. self.cmd('extract', self.repository_location + '::test')
  734. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'tagged3'])
  735. def test_exclude_tagged(self):
  736. self._create_test_tagged()
  737. self.cmd('create', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP', self.repository_location + '::test', 'input')
  738. self._assert_test_tagged()
  739. def test_recreate_exclude_tagged(self):
  740. self._create_test_tagged()
  741. self.cmd('create', self.repository_location + '::test', 'input')
  742. self.cmd('recreate', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP',
  743. self.repository_location + '::test')
  744. self._assert_test_tagged()
  745. def _create_test_keep_tagged(self):
  746. self.cmd('init', self.repository_location)
  747. self.create_regular_file('file0', size=1024)
  748. self.create_regular_file('tagged1/.NOBACKUP1')
  749. self.create_regular_file('tagged1/file1', size=1024)
  750. self.create_regular_file('tagged2/.NOBACKUP2')
  751. self.create_regular_file('tagged2/file2', size=1024)
  752. self.create_regular_file('tagged3/%s' % CACHE_TAG_NAME,
  753. contents=CACHE_TAG_CONTENTS + b' extra stuff')
  754. self.create_regular_file('tagged3/file3', size=1024)
  755. self.create_regular_file('taggedall/.NOBACKUP1')
  756. self.create_regular_file('taggedall/.NOBACKUP2')
  757. self.create_regular_file('taggedall/%s' % CACHE_TAG_NAME,
  758. contents=CACHE_TAG_CONTENTS + b' extra stuff')
  759. self.create_regular_file('taggedall/file4', size=1024)
  760. def _assert_test_keep_tagged(self):
  761. with changedir('output'):
  762. self.cmd('extract', self.repository_location + '::test')
  763. self.assert_equal(sorted(os.listdir('output/input')), ['file0', 'tagged1', 'tagged2', 'tagged3', 'taggedall'])
  764. self.assert_equal(os.listdir('output/input/tagged1'), ['.NOBACKUP1'])
  765. self.assert_equal(os.listdir('output/input/tagged2'), ['.NOBACKUP2'])
  766. self.assert_equal(os.listdir('output/input/tagged3'), [CACHE_TAG_NAME])
  767. self.assert_equal(sorted(os.listdir('output/input/taggedall')),
  768. ['.NOBACKUP1', '.NOBACKUP2', CACHE_TAG_NAME, ])
  769. def test_exclude_keep_tagged(self):
  770. self._create_test_keep_tagged()
  771. self.cmd('create', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2',
  772. '--exclude-caches', '--keep-tag-files', self.repository_location + '::test', 'input')
  773. self._assert_test_keep_tagged()
  774. def test_recreate_exclude_keep_tagged(self):
  775. self._create_test_keep_tagged()
  776. self.cmd('create', self.repository_location + '::test', 'input')
  777. self.cmd('recreate', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2',
  778. '--exclude-caches', '--keep-tag-files', self.repository_location + '::test')
  779. self._assert_test_keep_tagged()
  780. @pytest.mark.skipif(not xattr.XATTR_FAKEROOT, reason='Linux capabilities test, requires fakeroot >= 1.20.2')
  781. def test_extract_capabilities(self):
  782. fchown = os.fchown
  783. # We need to manually patch chown to get the behaviour Linux has, since fakeroot does not
  784. # accurately model the interaction of chown(2) and Linux capabilities, i.e. it does not remove them.
  785. def patched_fchown(fd, uid, gid):
  786. xattr.setxattr(fd, 'security.capability', None, follow_symlinks=False)
  787. fchown(fd, uid, gid)
  788. # The capability descriptor used here is valid and taken from a /usr/bin/ping
  789. capabilities = b'\x01\x00\x00\x02\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
  790. self.create_regular_file('file')
  791. xattr.setxattr('input/file', 'security.capability', capabilities)
  792. self.cmd('init', self.repository_location)
  793. self.cmd('create', self.repository_location + '::test', 'input')
  794. with changedir('output'):
  795. with patch.object(os, 'fchown', patched_fchown):
  796. self.cmd('extract', self.repository_location + '::test')
  797. assert xattr.getxattr('input/file', 'security.capability') == capabilities
  798. def test_path_normalization(self):
  799. self.cmd('init', self.repository_location)
  800. self.create_regular_file('dir1/dir2/file', size=1024 * 80)
  801. with changedir('input/dir1/dir2'):
  802. self.cmd('create', self.repository_location + '::test', '../../../input/dir1/../dir1/dir2/..')
  803. output = self.cmd('list', self.repository_location + '::test')
  804. self.assert_not_in('..', output)
  805. self.assert_in(' input/dir1/dir2/file', output)
  806. def test_exclude_normalization(self):
  807. self.cmd('init', self.repository_location)
  808. self.create_regular_file('file1', size=1024 * 80)
  809. self.create_regular_file('file2', size=1024 * 80)
  810. with changedir('input'):
  811. self.cmd('create', '--exclude=file1', self.repository_location + '::test1', '.')
  812. with changedir('output'):
  813. self.cmd('extract', self.repository_location + '::test1')
  814. self.assert_equal(sorted(os.listdir('output')), ['file2'])
  815. with changedir('input'):
  816. self.cmd('create', '--exclude=./file1', self.repository_location + '::test2', '.')
  817. with changedir('output'):
  818. self.cmd('extract', self.repository_location + '::test2')
  819. self.assert_equal(sorted(os.listdir('output')), ['file2'])
  820. self.cmd('create', '--exclude=input/./file1', self.repository_location + '::test3', 'input')
  821. with changedir('output'):
  822. self.cmd('extract', self.repository_location + '::test3')
  823. self.assert_equal(sorted(os.listdir('output/input')), ['file2'])
  824. def test_repeated_files(self):
  825. self.create_regular_file('file1', size=1024 * 80)
  826. self.cmd('init', self.repository_location)
  827. self.cmd('create', self.repository_location + '::test', 'input', 'input')
  828. def test_overwrite(self):
  829. self.create_regular_file('file1', size=1024 * 80)
  830. self.create_regular_file('dir2/file2', size=1024 * 80)
  831. self.cmd('init', self.repository_location)
  832. self.cmd('create', self.repository_location + '::test', 'input')
  833. # Overwriting regular files and directories should be supported
  834. os.mkdir('output/input')
  835. os.mkdir('output/input/file1')
  836. os.mkdir('output/input/dir2')
  837. with changedir('output'):
  838. self.cmd('extract', self.repository_location + '::test')
  839. self.assert_dirs_equal('input', 'output/input')
  840. # But non-empty dirs should fail
  841. os.unlink('output/input/file1')
  842. os.mkdir('output/input/file1')
  843. os.mkdir('output/input/file1/dir')
  844. with changedir('output'):
  845. self.cmd('extract', self.repository_location + '::test', exit_code=1)
  846. def test_rename(self):
  847. self.create_regular_file('file1', size=1024 * 80)
  848. self.create_regular_file('dir2/file2', size=1024 * 80)
  849. self.cmd('init', self.repository_location)
  850. self.cmd('create', self.repository_location + '::test', 'input')
  851. self.cmd('create', self.repository_location + '::test.2', 'input')
  852. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  853. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  854. self.cmd('rename', self.repository_location + '::test', 'test.3')
  855. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  856. self.cmd('rename', self.repository_location + '::test.2', 'test.4')
  857. self.cmd('extract', '--dry-run', self.repository_location + '::test.3')
  858. self.cmd('extract', '--dry-run', self.repository_location + '::test.4')
  859. # Make sure both archives have been renamed
  860. with Repository(self.repository_path) as repository:
  861. manifest, key = Manifest.load(repository)
  862. self.assert_equal(len(manifest.archives), 2)
  863. self.assert_in('test.3', manifest.archives)
  864. self.assert_in('test.4', manifest.archives)
  865. def test_info(self):
  866. self.create_regular_file('file1', size=1024 * 80)
  867. self.cmd('init', self.repository_location)
  868. self.cmd('create', self.repository_location + '::test', 'input')
  869. info_repo = self.cmd('info', self.repository_location)
  870. assert 'All archives:' in info_repo
  871. info_archive = self.cmd('info', self.repository_location + '::test')
  872. assert 'Archive name: test\n' in info_archive
  873. info_archive = self.cmd('info', '--first', '1', self.repository_location)
  874. assert 'Archive name: test\n' in info_archive
  875. def test_comment(self):
  876. self.create_regular_file('file1', size=1024 * 80)
  877. self.cmd('init', self.repository_location)
  878. self.cmd('create', self.repository_location + '::test1', 'input')
  879. self.cmd('create', '--comment', 'this is the comment', self.repository_location + '::test2', 'input')
  880. self.cmd('create', '--comment', '"deleted" comment', self.repository_location + '::test3', 'input')
  881. self.cmd('create', '--comment', 'preserved comment', self.repository_location + '::test4', 'input')
  882. assert 'Comment: \n' in self.cmd('info', self.repository_location + '::test1')
  883. assert 'Comment: this is the comment' in self.cmd('info', self.repository_location + '::test2')
  884. self.cmd('recreate', self.repository_location + '::test1', '--comment', 'added comment')
  885. self.cmd('recreate', self.repository_location + '::test2', '--comment', 'modified comment')
  886. self.cmd('recreate', self.repository_location + '::test3', '--comment', '')
  887. self.cmd('recreate', self.repository_location + '::test4', '12345')
  888. assert 'Comment: added comment' in self.cmd('info', self.repository_location + '::test1')
  889. assert 'Comment: modified comment' in self.cmd('info', self.repository_location + '::test2')
  890. assert 'Comment: \n' in self.cmd('info', self.repository_location + '::test3')
  891. assert 'Comment: preserved comment' in self.cmd('info', self.repository_location + '::test4')
  892. def test_delete(self):
  893. self.create_regular_file('file1', size=1024 * 80)
  894. self.create_regular_file('dir2/file2', size=1024 * 80)
  895. self.cmd('init', self.repository_location)
  896. self.cmd('create', self.repository_location + '::test', 'input')
  897. self.cmd('create', self.repository_location + '::test.2', 'input')
  898. self.cmd('create', self.repository_location + '::test.3', 'input')
  899. self.cmd('create', self.repository_location + '::another_test.1', 'input')
  900. self.cmd('create', self.repository_location + '::another_test.2', 'input')
  901. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  902. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  903. self.cmd('delete', '--prefix', 'another_', self.repository_location)
  904. self.cmd('delete', '--last', '1', self.repository_location)
  905. self.cmd('delete', self.repository_location + '::test')
  906. self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
  907. output = self.cmd('delete', '--stats', self.repository_location + '::test.2')
  908. self.assert_in('Deleted data:', output)
  909. # Make sure all data except the manifest has been deleted
  910. with Repository(self.repository_path) as repository:
  911. self.assert_equal(len(repository), 1)
  912. def test_delete_repo(self):
  913. self.create_regular_file('file1', size=1024 * 80)
  914. self.create_regular_file('dir2/file2', size=1024 * 80)
  915. self.cmd('init', self.repository_location)
  916. self.cmd('create', self.repository_location + '::test', 'input')
  917. self.cmd('create', self.repository_location + '::test.2', 'input')
  918. os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'no'
  919. self.cmd('delete', self.repository_location, exit_code=2)
  920. assert os.path.exists(self.repository_path)
  921. os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
  922. self.cmd('delete', self.repository_location)
  923. # Make sure the repo is gone
  924. self.assertFalse(os.path.exists(self.repository_path))
  925. def test_corrupted_repository(self):
  926. self.cmd('init', self.repository_location)
  927. self.create_src_archive('test')
  928. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  929. output = self.cmd('check', '--show-version', self.repository_location)
  930. self.assert_in('borgbackup version', output) # implied output even without --info given
  931. self.assert_not_in('Starting repository check', output) # --info not given for root logger
  932. name = sorted(os.listdir(os.path.join(self.tmpdir, 'repository', 'data', '0')), reverse=True)[1]
  933. with open(os.path.join(self.tmpdir, 'repository', 'data', '0', name), 'r+b') as fd:
  934. fd.seek(100)
  935. fd.write(b'XXXX')
  936. output = self.cmd('check', '--info', self.repository_location, exit_code=1)
  937. self.assert_in('Starting repository check', output) # --info given for root logger
  938. # we currently need to be able to create a lock directory inside the repo:
  939. @pytest.mark.xfail(reason="we need to be able to create the lock directory inside the repo")
  940. def test_readonly_repository(self):
  941. self.cmd('init', self.repository_location)
  942. self.create_src_archive('test')
  943. os.system('chmod -R ugo-w ' + self.repository_path)
  944. try:
  945. self.cmd('extract', '--dry-run', self.repository_location + '::test')
  946. finally:
  947. # Restore permissions so shutil.rmtree is able to delete it
  948. os.system('chmod -R u+w ' + self.repository_path)
  949. @pytest.mark.skipif('BORG_TESTS_IGNORE_MODES' in os.environ, reason='modes unreliable')
  950. def test_umask(self):
  951. self.create_regular_file('file1', size=1024 * 80)
  952. self.cmd('init', self.repository_location)
  953. self.cmd('create', self.repository_location + '::test', 'input')
  954. mode = os.stat(self.repository_path).st_mode
  955. self.assertEqual(stat.S_IMODE(mode), 0o700)
  956. def test_create_dry_run(self):
  957. self.cmd('init', self.repository_location)
  958. self.cmd('create', '--dry-run', self.repository_location + '::test', 'input')
  959. # Make sure no archive has been created
  960. with Repository(self.repository_path) as repository:
  961. manifest, key = Manifest.load(repository)
  962. self.assert_equal(len(manifest.archives), 0)
  963. def test_progress_on(self):
  964. self.create_regular_file('file1', size=1024 * 80)
  965. self.cmd('init', self.repository_location)
  966. output = self.cmd('create', '--progress', self.repository_location + '::test4', 'input')
  967. self.assert_in("\r", output)
  968. def test_progress_off(self):
  969. self.create_regular_file('file1', size=1024 * 80)
  970. self.cmd('init', self.repository_location)
  971. output = self.cmd('create', self.repository_location + '::test5', 'input')
  972. self.assert_not_in("\r", output)
  973. def test_file_status(self):
  974. """test that various file status show expected results
  975. clearly incomplete: only tests for the weird "unchanged" status for now"""
  976. now = time.time()
  977. self.create_regular_file('file1', size=1024 * 80)
  978. os.utime('input/file1', (now - 5, now - 5)) # 5 seconds ago
  979. self.create_regular_file('file2', size=1024 * 80)
  980. self.cmd('init', self.repository_location)
  981. output = self.cmd('create', '--list', self.repository_location + '::test', 'input')
  982. self.assert_in("A input/file1", output)
  983. self.assert_in("A input/file2", output)
  984. # should find first file as unmodified
  985. output = self.cmd('create', '--list', self.repository_location + '::test1', 'input')
  986. self.assert_in("U input/file1", output)
  987. # this is expected, although surprising, for why, see:
  988. # https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file
  989. self.assert_in("A input/file2", output)
  990. def test_file_status_excluded(self):
  991. """test that excluded paths are listed"""
  992. now = time.time()
  993. self.create_regular_file('file1', size=1024 * 80)
  994. os.utime('input/file1', (now - 5, now - 5)) # 5 seconds ago
  995. self.create_regular_file('file2', size=1024 * 80)
  996. if has_lchflags:
  997. self.create_regular_file('file3', size=1024 * 80)
  998. platform.set_flags(os.path.join(self.input_path, 'file3'), stat.UF_NODUMP)
  999. self.cmd('init', self.repository_location)
  1000. output = self.cmd('create', '--list', self.repository_location + '::test', 'input')
  1001. self.assert_in("A input/file1", output)
  1002. self.assert_in("A input/file2", output)
  1003. if has_lchflags:
  1004. self.assert_in("x input/file3", output)
  1005. # should find second file as excluded
  1006. output = self.cmd('create', '--list', self.repository_location + '::test1', 'input', '--exclude', '*/file2')
  1007. self.assert_in("U input/file1", output)
  1008. self.assert_in("x input/file2", output)
  1009. if has_lchflags:
  1010. self.assert_in("x input/file3", output)
  1011. def test_create_topical(self):
  1012. now = time.time()
  1013. self.create_regular_file('file1', size=1024 * 80)
  1014. os.utime('input/file1', (now-5, now-5))
  1015. self.create_regular_file('file2', size=1024 * 80)
  1016. self.cmd('init', self.repository_location)
  1017. # no listing by default
  1018. output = self.cmd('create', self.repository_location + '::test', 'input')
  1019. self.assert_not_in('file1', output)
  1020. # shouldn't be listed even if unchanged
  1021. output = self.cmd('create', self.repository_location + '::test0', 'input')
  1022. self.assert_not_in('file1', output)
  1023. # should list the file as unchanged
  1024. output = self.cmd('create', '--list', '--filter=U', self.repository_location + '::test1', 'input')
  1025. self.assert_in('file1', output)
  1026. # should *not* list the file as changed
  1027. output = self.cmd('create', '--list', '--filter=AM', self.repository_location + '::test2', 'input')
  1028. self.assert_not_in('file1', output)
  1029. # change the file
  1030. self.create_regular_file('file1', size=1024 * 100)
  1031. # should list the file as changed
  1032. output = self.cmd('create', '--list', '--filter=AM', self.repository_location + '::test3', 'input')
  1033. self.assert_in('file1', output)
  1034. def test_create_read_special_broken_symlink(self):
  1035. os.symlink('somewhere doesnt exist', os.path.join(self.input_path, 'link'))
  1036. self.cmd('init', self.repository_location)
  1037. archive = self.repository_location + '::test'
  1038. self.cmd('create', '--read-special', archive, 'input')
  1039. output = self.cmd('list', archive)
  1040. assert 'input/link -> somewhere doesnt exist' in output
  1041. # def test_cmdline_compatibility(self):
  1042. # self.create_regular_file('file1', size=1024 * 80)
  1043. # self.cmd('init', self.repository_location)
  1044. # self.cmd('create', self.repository_location + '::test', 'input')
  1045. # output = self.cmd('foo', self.repository_location, '--old')
  1046. # self.assert_in('"--old" has been deprecated. Use "--new" instead', output)
  1047. def test_prune_repository(self):
  1048. self.cmd('init', self.repository_location)
  1049. self.cmd('create', self.repository_location + '::test1', src_dir)
  1050. self.cmd('create', self.repository_location + '::test2', src_dir)
  1051. # these are not really a checkpoints, but they look like some:
  1052. self.cmd('create', self.repository_location + '::test3.checkpoint', src_dir)
  1053. self.cmd('create', self.repository_location + '::test3.checkpoint.1', src_dir)
  1054. self.cmd('create', self.repository_location + '::test4.checkpoint', src_dir)
  1055. output = self.cmd('prune', '--list', '--dry-run', self.repository_location, '--keep-daily=2')
  1056. self.assert_in('Keeping archive: test2', output)
  1057. self.assert_in('Would prune: test1', output)
  1058. # must keep the latest non-checkpoint archive:
  1059. self.assert_in('Keeping archive: test2', output)
  1060. # must keep the latest checkpoint archive:
  1061. self.assert_in('Keeping archive: test4.checkpoint', output)
  1062. output = self.cmd('list', self.repository_location)
  1063. self.assert_in('test1', output)
  1064. self.assert_in('test2', output)
  1065. self.assert_in('test3.checkpoint', output)
  1066. self.assert_in('test3.checkpoint.1', output)
  1067. self.assert_in('test4.checkpoint', output)
  1068. self.cmd('prune', self.repository_location, '--keep-daily=2')
  1069. output = self.cmd('list', self.repository_location)
  1070. self.assert_not_in('test1', output)
  1071. # the latest non-checkpoint archive must be still there:
  1072. self.assert_in('test2', output)
  1073. # only the latest checkpoint archive must still be there:
  1074. self.assert_not_in('test3.checkpoint', output)
  1075. self.assert_not_in('test3.checkpoint.1', output)
  1076. self.assert_in('test4.checkpoint', output)
  1077. # now we supercede the latest checkpoint by a successful backup:
  1078. self.cmd('create', self.repository_location + '::test5', src_dir)
  1079. self.cmd('prune', self.repository_location, '--keep-daily=2')
  1080. output = self.cmd('list', self.repository_location)
  1081. # all checkpoints should be gone now:
  1082. self.assert_not_in('checkpoint', output)
  1083. # the latest archive must be still there
  1084. self.assert_in('test5', output)
  1085. def test_prune_repository_save_space(self):
  1086. self.cmd('init', self.repository_location)
  1087. self.cmd('create', self.repository_location + '::test1', src_dir)
  1088. self.cmd('create', self.repository_location + '::test2', src_dir)
  1089. output = self.cmd('prune', '--list', '--stats', '--dry-run', self.repository_location, '--keep-daily=2')
  1090. self.assert_in('Keeping archive: test2', output)
  1091. self.assert_in('Would prune: test1', output)
  1092. self.assert_in('Deleted data:', output)
  1093. output = self.cmd('list', self.repository_location)
  1094. self.assert_in('test1', output)
  1095. self.assert_in('test2', output)
  1096. self.cmd('prune', '--save-space', self.repository_location, '--keep-daily=2')
  1097. output = self.cmd('list', self.repository_location)
  1098. self.assert_not_in('test1', output)
  1099. self.assert_in('test2', output)
  1100. def test_prune_repository_prefix(self):
  1101. self.cmd('init', self.repository_location)
  1102. self.cmd('create', self.repository_location + '::foo-2015-08-12-10:00', src_dir)
  1103. self.cmd('create', self.repository_location + '::foo-2015-08-12-20:00', src_dir)
  1104. self.cmd('create', self.repository_location + '::bar-2015-08-12-10:00', src_dir)
  1105. self.cmd('create', self.repository_location + '::bar-2015-08-12-20:00', src_dir)
  1106. output = self.cmd('prune', '--list', '--dry-run', self.repository_location, '--keep-daily=2', '--prefix=foo-')
  1107. self.assert_in('Keeping archive: foo-2015-08-12-20:00', output)
  1108. self.assert_in('Would prune: foo-2015-08-12-10:00', output)
  1109. output = self.cmd('list', self.repository_location)
  1110. self.assert_in('foo-2015-08-12-10:00', output)
  1111. self.assert_in('foo-2015-08-12-20:00', output)
  1112. self.assert_in('bar-2015-08-12-10:00', output)
  1113. self.assert_in('bar-2015-08-12-20:00', output)
  1114. self.cmd('prune', self.repository_location, '--keep-daily=2', '--prefix=foo-')
  1115. output = self.cmd('list', self.repository_location)
  1116. self.assert_not_in('foo-2015-08-12-10:00', output)
  1117. self.assert_in('foo-2015-08-12-20:00', output)
  1118. self.assert_in('bar-2015-08-12-10:00', output)
  1119. self.assert_in('bar-2015-08-12-20:00', output)
  1120. def test_list_prefix(self):
  1121. self.cmd('init', self.repository_location)
  1122. self.cmd('create', self.repository_location + '::test-1', src_dir)
  1123. self.cmd('create', self.repository_location + '::something-else-than-test-1', src_dir)
  1124. self.cmd('create', self.repository_location + '::test-2', src_dir)
  1125. output = self.cmd('list', '--prefix=test-', self.repository_location)
  1126. self.assert_in('test-1', output)
  1127. self.assert_in('test-2', output)
  1128. self.assert_not_in('something-else', output)
  1129. def test_list_format(self):
  1130. self.cmd('init', self.repository_location)
  1131. test_archive = self.repository_location + '::test'
  1132. self.cmd('create', test_archive, src_dir)
  1133. output_warn = self.cmd('list', '--list-format', '-', test_archive)
  1134. self.assert_in('--list-format" has been deprecated.', output_warn)
  1135. output_1 = self.cmd('list', test_archive)
  1136. output_2 = self.cmd('list', '--format', '{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}', test_archive)
  1137. output_3 = self.cmd('list', '--format', '{mtime:%s} {path}{NL}', test_archive)
  1138. self.assertEqual(output_1, output_2)
  1139. self.assertNotEqual(output_1, output_3)
  1140. def test_list_repository_format(self):
  1141. self.cmd('init', self.repository_location)
  1142. self.cmd('create', self.repository_location + '::test-1', src_dir)
  1143. self.cmd('create', self.repository_location + '::test-2', src_dir)
  1144. output_1 = self.cmd('list', self.repository_location)
  1145. output_2 = self.cmd('list', '--format', '{archive:<36} {time} [{id}]{NL}', self.repository_location)
  1146. self.assertEqual(output_1, output_2)
  1147. output_1 = self.cmd('list', '--short', self.repository_location)
  1148. self.assertEqual(output_1, 'test-1\ntest-2\n')
  1149. output_1 = self.cmd('list', '--format', '{barchive}/', self.repository_location)
  1150. self.assertEqual(output_1, 'test-1/test-2/')
  1151. def test_list_hash(self):
  1152. self.create_regular_file('empty_file', size=0)
  1153. self.create_regular_file('amb', contents=b'a' * 1000000)
  1154. self.cmd('init', self.repository_location)
  1155. test_archive = self.repository_location + '::test'
  1156. self.cmd('create', test_archive, 'input')
  1157. output = self.cmd('list', '--format', '{sha256} {path}{NL}', test_archive)
  1158. assert "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0 input/amb" in output
  1159. assert "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 input/empty_file" in output
  1160. def test_list_chunk_counts(self):
  1161. self.create_regular_file('empty_file', size=0)
  1162. self.create_regular_file('two_chunks')
  1163. with open(os.path.join(self.input_path, 'two_chunks'), 'wb') as fd:
  1164. fd.write(b'abba' * 2000000)
  1165. fd.write(b'baab' * 2000000)
  1166. self.cmd('init', self.repository_location)
  1167. test_archive = self.repository_location + '::test'
  1168. self.cmd('create', test_archive, 'input')
  1169. output = self.cmd('list', '--format', '{num_chunks} {unique_chunks} {path}{NL}', test_archive)
  1170. assert "0 0 input/empty_file" in output
  1171. assert "2 2 input/two_chunks" in output
  1172. def test_list_size(self):
  1173. self.create_regular_file('compressible_file', size=10000)
  1174. self.cmd('init', self.repository_location)
  1175. test_archive = self.repository_location + '::test'
  1176. self.cmd('create', '-C', 'lz4', test_archive, 'input')
  1177. output = self.cmd('list', '--format', '{size} {csize} {path}{NL}', test_archive)
  1178. size, csize, path = output.split("\n")[1].split(" ")
  1179. assert int(csize) < int(size)
  1180. def _get_sizes(self, compression, compressible, size=10000):
  1181. if compressible:
  1182. contents = b'X' * size
  1183. else:
  1184. contents = os.urandom(size)
  1185. self.create_regular_file('file', contents=contents)
  1186. self.cmd('init', '--encryption=none', self.repository_location)
  1187. archive = self.repository_location + '::test'
  1188. self.cmd('create', '-C', compression, archive, 'input')
  1189. output = self.cmd('list', '--format', '{size} {csize} {path}{NL}', archive)
  1190. size, csize, path = output.split("\n")[1].split(" ")
  1191. return int(size), int(csize)
  1192. def test_compression_none_compressible(self):
  1193. size, csize = self._get_sizes('none', compressible=True)
  1194. assert csize >= size
  1195. assert csize == size + 3
  1196. def test_compression_none_uncompressible(self):
  1197. size, csize = self._get_sizes('none', compressible=False)
  1198. assert csize >= size
  1199. assert csize == size + 3
  1200. def test_compression_zlib_compressible(self):
  1201. size, csize = self._get_sizes('zlib', compressible=True)
  1202. assert csize < size * 0.1
  1203. assert csize == 35
  1204. def test_compression_zlib_uncompressible(self):
  1205. size, csize = self._get_sizes('zlib', compressible=False)
  1206. assert csize >= size
  1207. def test_compression_auto_compressible(self):
  1208. size, csize = self._get_sizes('auto,zlib', compressible=True)
  1209. assert csize < size * 0.1
  1210. assert csize == 35 # same as compression 'zlib'
  1211. def test_compression_auto_uncompressible(self):
  1212. size, csize = self._get_sizes('auto,zlib', compressible=False)
  1213. assert csize >= size
  1214. assert csize == size + 3 # same as compression 'none'
  1215. def test_compression_lz4_compressible(self):
  1216. size, csize = self._get_sizes('lz4', compressible=True)
  1217. assert csize < size * 0.1
  1218. def test_compression_lz4_uncompressible(self):
  1219. size, csize = self._get_sizes('lz4', compressible=False)
  1220. assert csize >= size
  1221. def test_compression_lzma_compressible(self):
  1222. size, csize = self._get_sizes('lzma', compressible=True)
  1223. assert csize < size * 0.1
  1224. def test_compression_lzma_uncompressible(self):
  1225. size, csize = self._get_sizes('lzma', compressible=False)
  1226. assert csize >= size
  1227. def test_break_lock(self):
  1228. self.cmd('init', self.repository_location)
  1229. self.cmd('break-lock', self.repository_location)
  1230. def test_usage(self):
  1231. if self.FORK_DEFAULT:
  1232. self.cmd(exit_code=0)
  1233. self.cmd('-h', exit_code=0)
  1234. else:
  1235. self.assert_raises(SystemExit, lambda: self.cmd())
  1236. self.assert_raises(SystemExit, lambda: self.cmd('-h'))
  1237. def test_help(self):
  1238. assert 'Borg' in self.cmd('help')
  1239. assert 'patterns' in self.cmd('help', 'patterns')
  1240. assert 'Initialize' in self.cmd('help', 'init')
  1241. assert 'positional arguments' not in self.cmd('help', 'init', '--epilog-only')
  1242. assert 'This command initializes' not in self.cmd('help', 'init', '--usage-only')
  1243. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  1244. def test_fuse(self):
  1245. def has_noatime(some_file):
  1246. atime_before = os.stat(some_file).st_atime_ns
  1247. try:
  1248. os.close(os.open(some_file, flags_noatime))
  1249. except PermissionError:
  1250. return False
  1251. else:
  1252. atime_after = os.stat(some_file).st_atime_ns
  1253. noatime_used = flags_noatime != flags_normal
  1254. return noatime_used and atime_before == atime_after
  1255. self.cmd('init', self.repository_location)
  1256. self.create_test_files()
  1257. have_noatime = has_noatime('input/file1')
  1258. self.cmd('create', self.repository_location + '::archive', 'input')
  1259. self.cmd('create', self.repository_location + '::archive2', 'input')
  1260. if has_lchflags:
  1261. # remove the file we did not backup, so input and output become equal
  1262. os.remove(os.path.join('input', 'flagfile'))
  1263. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  1264. # mount the whole repository, archive contents shall show up in archivename subdirs of mountpoint:
  1265. with self.fuse_mount(self.repository_location, mountpoint):
  1266. # bsdflags are not supported by the FUSE mount
  1267. # we also ignore xattrs here, they are tested separately
  1268. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive', 'input'),
  1269. ignore_bsdflags=True, ignore_xattrs=True)
  1270. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'archive2', 'input'),
  1271. ignore_bsdflags=True, ignore_xattrs=True)
  1272. # mount only 1 archive, its contents shall show up directly in mountpoint:
  1273. with self.fuse_mount(self.repository_location + '::archive', mountpoint):
  1274. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'input'),
  1275. ignore_bsdflags=True, ignore_xattrs=True)
  1276. # regular file
  1277. in_fn = 'input/file1'
  1278. out_fn = os.path.join(mountpoint, 'input', 'file1')
  1279. # stat
  1280. sti1 = os.stat(in_fn)
  1281. sto1 = os.stat(out_fn)
  1282. assert sti1.st_mode == sto1.st_mode
  1283. assert sti1.st_uid == sto1.st_uid
  1284. assert sti1.st_gid == sto1.st_gid
  1285. assert sti1.st_size == sto1.st_size
  1286. if have_noatime:
  1287. assert sti1.st_atime == sto1.st_atime
  1288. assert sti1.st_ctime == sto1.st_ctime
  1289. assert sti1.st_mtime == sto1.st_mtime
  1290. # note: there is another hardlink to this, see below
  1291. assert sti1.st_nlink == sto1.st_nlink == 2
  1292. # read
  1293. with open(in_fn, 'rb') as in_f, open(out_fn, 'rb') as out_f:
  1294. assert in_f.read() == out_f.read()
  1295. # hardlink (to 'input/file1')
  1296. if are_hardlinks_supported():
  1297. in_fn = 'input/hardlink'
  1298. out_fn = os.path.join(mountpoint, 'input', 'hardlink')
  1299. sti2 = os.stat(in_fn)
  1300. sto2 = os.stat(out_fn)
  1301. assert sti2.st_nlink == sto2.st_nlink == 2
  1302. assert sto1.st_ino == sto2.st_ino
  1303. # symlink
  1304. if are_symlinks_supported():
  1305. in_fn = 'input/link1'
  1306. out_fn = os.path.join(mountpoint, 'input', 'link1')
  1307. sti = os.stat(in_fn, follow_symlinks=False)
  1308. sto = os.stat(out_fn, follow_symlinks=False)
  1309. assert stat.S_ISLNK(sti.st_mode)
  1310. assert stat.S_ISLNK(sto.st_mode)
  1311. assert os.readlink(in_fn) == os.readlink(out_fn)
  1312. # FIFO
  1313. if are_fifos_supported():
  1314. out_fn = os.path.join(mountpoint, 'input', 'fifo1')
  1315. sto = os.stat(out_fn)
  1316. assert stat.S_ISFIFO(sto.st_mode)
  1317. # list/read xattrs
  1318. try:
  1319. in_fn = 'input/fusexattr'
  1320. out_fn = os.path.join(mountpoint, 'input', 'fusexattr')
  1321. if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path):
  1322. assert no_selinux(xattr.listxattr(out_fn)) == ['user.foo', ]
  1323. assert xattr.getxattr(out_fn, 'user.foo') == b'bar'
  1324. else:
  1325. assert xattr.listxattr(out_fn) == []
  1326. try:
  1327. xattr.getxattr(out_fn, 'user.foo')
  1328. except OSError as e:
  1329. assert e.errno == llfuse.ENOATTR
  1330. else:
  1331. assert False, "expected OSError(ENOATTR), but no error was raised"
  1332. except OSError as err:
  1333. if sys.platform.startswith(('freebsd', )) and err.errno == errno.ENOTSUP:
  1334. # some systems have no xattr support on FUSE
  1335. pass
  1336. else:
  1337. raise
  1338. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  1339. def test_fuse_versions_view(self):
  1340. self.cmd('init', self.repository_location)
  1341. self.create_regular_file('test', contents=b'first')
  1342. if are_hardlinks_supported():
  1343. self.create_regular_file('hardlink1', contents=b'')
  1344. os.link('input/hardlink1', 'input/hardlink2')
  1345. self.cmd('create', self.repository_location + '::archive1', 'input')
  1346. self.create_regular_file('test', contents=b'second')
  1347. self.cmd('create', self.repository_location + '::archive2', 'input')
  1348. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  1349. # mount the whole repository, archive contents shall show up in versioned view:
  1350. with self.fuse_mount(self.repository_location, mountpoint, '-o', 'versions'):
  1351. path = os.path.join(mountpoint, 'input', 'test') # filename shows up as directory ...
  1352. files = os.listdir(path)
  1353. assert all(f.startswith('test.') for f in files) # ... with files test.xxxxxxxx in there
  1354. assert {b'first', b'second'} == {open(os.path.join(path, f), 'rb').read() for f in files}
  1355. if are_hardlinks_supported():
  1356. st1 = os.stat(os.path.join(mountpoint, 'input', 'hardlink1', 'hardlink1.00000000'))
  1357. st2 = os.stat(os.path.join(mountpoint, 'input', 'hardlink2', 'hardlink2.00000000'))
  1358. assert st1.st_ino == st2.st_ino
  1359. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  1360. def test_fuse_allow_damaged_files(self):
  1361. self.cmd('init', self.repository_location)
  1362. self.create_src_archive('archive')
  1363. # Get rid of a chunk and repair it
  1364. archive, repository = self.open_archive('archive')
  1365. with repository:
  1366. for item in archive.iter_items():
  1367. if item.path.endswith('testsuite/archiver.py'):
  1368. repository.delete(item.chunks[-1].id)
  1369. path = item.path # store full path for later
  1370. break
  1371. else:
  1372. assert False # missed the file
  1373. repository.commit()
  1374. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1375. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  1376. with self.fuse_mount(self.repository_location + '::archive', mountpoint):
  1377. with pytest.raises(OSError) as excinfo:
  1378. open(os.path.join(mountpoint, path))
  1379. assert excinfo.value.errno == errno.EIO
  1380. with self.fuse_mount(self.repository_location + '::archive', mountpoint, '-o', 'allow_damaged_files'):
  1381. open(os.path.join(mountpoint, path)).close()
  1382. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  1383. def test_fuse_mount_options(self):
  1384. self.cmd('init', self.repository_location)
  1385. self.create_src_archive('arch11')
  1386. self.create_src_archive('arch12')
  1387. self.create_src_archive('arch21')
  1388. self.create_src_archive('arch22')
  1389. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  1390. with self.fuse_mount(self.repository_location, mountpoint, '--first=2', '--sort=name'):
  1391. assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch11', 'arch12']
  1392. with self.fuse_mount(self.repository_location, mountpoint, '--last=2', '--sort=name'):
  1393. assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch21', 'arch22']
  1394. with self.fuse_mount(self.repository_location, mountpoint, '--prefix=arch1'):
  1395. assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch11', 'arch12']
  1396. with self.fuse_mount(self.repository_location, mountpoint, '--prefix=arch2'):
  1397. assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch21', 'arch22']
  1398. with self.fuse_mount(self.repository_location, mountpoint, '--prefix=arch'):
  1399. assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch11', 'arch12', 'arch21', 'arch22']
  1400. with self.fuse_mount(self.repository_location, mountpoint, '--prefix=nope'):
  1401. assert sorted(os.listdir(os.path.join(mountpoint))) == []
  1402. def verify_aes_counter_uniqueness(self, method):
  1403. seen = set() # Chunks already seen
  1404. used = set() # counter values already used
  1405. def verify_uniqueness():
  1406. with Repository(self.repository_path) as repository:
  1407. for key, _ in repository.open_index(repository.get_transaction_id()).iteritems():
  1408. data = repository.get(key)
  1409. hash = sha256(data).digest()
  1410. if hash not in seen:
  1411. seen.add(hash)
  1412. num_blocks = num_aes_blocks(len(data) - 41)
  1413. nonce = bytes_to_long(data[33:41])
  1414. for counter in range(nonce, nonce + num_blocks):
  1415. self.assert_not_in(counter, used)
  1416. used.add(counter)
  1417. self.create_test_files()
  1418. os.environ['BORG_PASSPHRASE'] = 'passphrase'
  1419. self.cmd('init', '--encryption=' + method, self.repository_location)
  1420. verify_uniqueness()
  1421. self.cmd('create', self.repository_location + '::test', 'input')
  1422. verify_uniqueness()
  1423. self.cmd('create', self.repository_location + '::test.2', 'input')
  1424. verify_uniqueness()
  1425. self.cmd('delete', self.repository_location + '::test.2')
  1426. verify_uniqueness()
  1427. def test_aes_counter_uniqueness_keyfile(self):
  1428. self.verify_aes_counter_uniqueness('keyfile')
  1429. def test_aes_counter_uniqueness_passphrase(self):
  1430. self.verify_aes_counter_uniqueness('repokey')
  1431. def test_debug_dump_archive_items(self):
  1432. self.create_test_files()
  1433. self.cmd('init', self.repository_location)
  1434. self.cmd('create', self.repository_location + '::test', 'input')
  1435. with changedir('output'):
  1436. output = self.cmd('debug', 'dump-archive-items', self.repository_location + '::test')
  1437. output_dir = sorted(os.listdir('output'))
  1438. assert len(output_dir) > 0 and output_dir[0].startswith('000000_')
  1439. assert 'Done.' in output
  1440. def test_debug_dump_repo_objs(self):
  1441. self.create_test_files()
  1442. self.cmd('init', self.repository_location)
  1443. self.cmd('create', self.repository_location + '::test', 'input')
  1444. with changedir('output'):
  1445. output = self.cmd('debug', 'dump-repo-objs', self.repository_location)
  1446. output_dir = sorted(os.listdir('output'))
  1447. assert len(output_dir) > 0 and output_dir[0].startswith('000000_')
  1448. assert 'Done.' in output
  1449. def test_debug_put_get_delete_obj(self):
  1450. self.cmd('init', self.repository_location)
  1451. data = b'some data'
  1452. hexkey = sha256(data).hexdigest()
  1453. self.create_regular_file('file', contents=data)
  1454. output = self.cmd('debug', 'put-obj', self.repository_location, 'input/file')
  1455. assert hexkey in output
  1456. output = self.cmd('debug', 'get-obj', self.repository_location, hexkey, 'output/file')
  1457. assert hexkey in output
  1458. with open('output/file', 'rb') as f:
  1459. data_read = f.read()
  1460. assert data == data_read
  1461. output = self.cmd('debug', 'delete-obj', self.repository_location, hexkey)
  1462. assert "deleted" in output
  1463. output = self.cmd('debug', 'delete-obj', self.repository_location, hexkey)
  1464. assert "not found" in output
  1465. output = self.cmd('debug', 'delete-obj', self.repository_location, 'invalid')
  1466. assert "is invalid" in output
  1467. def test_init_interrupt(self):
  1468. def raise_eof(*args):
  1469. raise EOFError
  1470. with patch.object(KeyfileKeyBase, 'create', raise_eof):
  1471. self.cmd('init', self.repository_location, exit_code=1)
  1472. assert not os.path.exists(self.repository_location)
  1473. def check_cache(self):
  1474. # First run a regular borg check
  1475. self.cmd('check', self.repository_location)
  1476. # Then check that the cache on disk matches exactly what's in the repo.
  1477. with self.open_repository() as repository:
  1478. manifest, key = Manifest.load(repository)
  1479. with Cache(repository, key, manifest, sync=False) as cache:
  1480. original_chunks = cache.chunks
  1481. cache.destroy(repository)
  1482. with Cache(repository, key, manifest) as cache:
  1483. correct_chunks = cache.chunks
  1484. assert original_chunks is not correct_chunks
  1485. seen = set()
  1486. for id, (refcount, size, csize) in correct_chunks.iteritems():
  1487. o_refcount, o_size, o_csize = original_chunks[id]
  1488. assert refcount == o_refcount
  1489. assert size == o_size
  1490. assert csize == o_csize
  1491. seen.add(id)
  1492. for id, (refcount, size, csize) in original_chunks.iteritems():
  1493. assert id in seen
  1494. def test_check_cache(self):
  1495. self.cmd('init', self.repository_location)
  1496. self.cmd('create', self.repository_location + '::test', 'input')
  1497. with self.open_repository() as repository:
  1498. manifest, key = Manifest.load(repository)
  1499. with Cache(repository, key, manifest, sync=False) as cache:
  1500. cache.begin_txn()
  1501. cache.chunks.incref(list(cache.chunks.iteritems())[0][0])
  1502. cache.commit()
  1503. with pytest.raises(AssertionError):
  1504. self.check_cache()
  1505. def test_recreate_target_rc(self):
  1506. self.cmd('init', self.repository_location)
  1507. output = self.cmd('recreate', self.repository_location, '--target=asdf', exit_code=2)
  1508. assert 'Need to specify single archive' in output
  1509. def test_recreate_target(self):
  1510. self.create_test_files()
  1511. self.cmd('init', self.repository_location)
  1512. self.check_cache()
  1513. archive = self.repository_location + '::test0'
  1514. self.cmd('create', archive, 'input')
  1515. self.check_cache()
  1516. original_archive = self.cmd('list', self.repository_location)
  1517. self.cmd('recreate', archive, 'input/dir2', '-e', 'input/dir2/file3', '--target=new-archive')
  1518. self.check_cache()
  1519. archives = self.cmd('list', self.repository_location)
  1520. assert original_archive in archives
  1521. assert 'new-archive' in archives
  1522. archive = self.repository_location + '::new-archive'
  1523. listing = self.cmd('list', '--short', archive)
  1524. assert 'file1' not in listing
  1525. assert 'dir2/file2' in listing
  1526. assert 'dir2/file3' not in listing
  1527. def test_recreate_basic(self):
  1528. self.create_test_files()
  1529. self.create_regular_file('dir2/file3', size=1024 * 80)
  1530. self.cmd('init', self.repository_location)
  1531. archive = self.repository_location + '::test0'
  1532. self.cmd('create', archive, 'input')
  1533. self.cmd('recreate', archive, 'input/dir2', '-e', 'input/dir2/file3')
  1534. self.check_cache()
  1535. listing = self.cmd('list', '--short', archive)
  1536. assert 'file1' not in listing
  1537. assert 'dir2/file2' in listing
  1538. assert 'dir2/file3' not in listing
  1539. @pytest.mark.skipif(not are_hardlinks_supported(), reason='hardlinks not supported')
  1540. def test_recreate_subtree_hardlinks(self):
  1541. # This is essentially the same problem set as in test_extract_hardlinks
  1542. self._extract_hardlinks_setup()
  1543. self.cmd('create', self.repository_location + '::test2', 'input')
  1544. self.cmd('recreate', self.repository_location + '::test', 'input/dir1')
  1545. self.check_cache()
  1546. with changedir('output'):
  1547. self.cmd('extract', self.repository_location + '::test')
  1548. assert os.stat('input/dir1/hardlink').st_nlink == 2
  1549. assert os.stat('input/dir1/subdir/hardlink').st_nlink == 2
  1550. assert os.stat('input/dir1/aaaa').st_nlink == 2
  1551. assert os.stat('input/dir1/source2').st_nlink == 2
  1552. with changedir('output'):
  1553. self.cmd('extract', self.repository_location + '::test2')
  1554. assert os.stat('input/dir1/hardlink').st_nlink == 4
  1555. def test_recreate_rechunkify(self):
  1556. with open(os.path.join(self.input_path, 'large_file'), 'wb') as fd:
  1557. fd.write(b'a' * 280)
  1558. fd.write(b'b' * 280)
  1559. self.cmd('init', self.repository_location)
  1560. self.cmd('create', '--chunker-params', '7,9,8,128', self.repository_location + '::test1', 'input')
  1561. self.cmd('create', self.repository_location + '::test2', 'input', '--no-files-cache')
  1562. list = self.cmd('list', self.repository_location + '::test1', 'input/large_file',
  1563. '--format', '{num_chunks} {unique_chunks}')
  1564. num_chunks, unique_chunks = map(int, list.split(' '))
  1565. # test1 and test2 do not deduplicate
  1566. assert num_chunks == unique_chunks
  1567. self.cmd('recreate', self.repository_location, '--chunker-params', 'default')
  1568. self.check_cache()
  1569. # test1 and test2 do deduplicate after recreate
  1570. assert not int(self.cmd('list', self.repository_location + '::test1', 'input/large_file',
  1571. '--format', '{unique_chunks}'))
  1572. def test_recreate_recompress(self):
  1573. self.create_regular_file('compressible', size=10000)
  1574. self.cmd('init', self.repository_location)
  1575. self.cmd('create', self.repository_location + '::test', 'input', '-C', 'none')
  1576. file_list = self.cmd('list', self.repository_location + '::test', 'input/compressible',
  1577. '--format', '{size} {csize} {sha256}')
  1578. size, csize, sha256_before = file_list.split(' ')
  1579. assert int(csize) >= int(size) # >= due to metadata overhead
  1580. self.cmd('recreate', self.repository_location, '-C', 'lz4')
  1581. self.check_cache()
  1582. file_list = self.cmd('list', self.repository_location + '::test', 'input/compressible',
  1583. '--format', '{size} {csize} {sha256}')
  1584. size, csize, sha256_after = file_list.split(' ')
  1585. assert int(csize) < int(size)
  1586. assert sha256_before == sha256_after
  1587. def test_recreate_dry_run(self):
  1588. self.create_regular_file('compressible', size=10000)
  1589. self.cmd('init', self.repository_location)
  1590. self.cmd('create', self.repository_location + '::test', 'input')
  1591. archives_before = self.cmd('list', self.repository_location + '::test')
  1592. self.cmd('recreate', self.repository_location, '-n', '-e', 'input/compressible')
  1593. self.check_cache()
  1594. archives_after = self.cmd('list', self.repository_location + '::test')
  1595. assert archives_after == archives_before
  1596. def test_recreate_skips_nothing_to_do(self):
  1597. self.create_regular_file('file1', size=1024 * 80)
  1598. self.cmd('init', self.repository_location)
  1599. self.cmd('create', self.repository_location + '::test', 'input')
  1600. info_before = self.cmd('info', self.repository_location + '::test')
  1601. self.cmd('recreate', self.repository_location, '--chunker-params', 'default')
  1602. self.check_cache()
  1603. info_after = self.cmd('info', self.repository_location + '::test')
  1604. assert info_before == info_after # includes archive ID
  1605. def test_with_lock(self):
  1606. self.cmd('init', self.repository_location)
  1607. lock_path = os.path.join(self.repository_path, 'lock.exclusive')
  1608. cmd = 'python3', '-c', 'import os, sys; sys.exit(42 if os.path.exists("%s") else 23)' % lock_path
  1609. self.cmd('with-lock', self.repository_location, *cmd, fork=True, exit_code=42)
  1610. def test_recreate_list_output(self):
  1611. self.cmd('init', self.repository_location)
  1612. self.create_regular_file('file1', size=0)
  1613. self.create_regular_file('file2', size=0)
  1614. self.create_regular_file('file3', size=0)
  1615. self.create_regular_file('file4', size=0)
  1616. self.create_regular_file('file5', size=0)
  1617. self.cmd('create', self.repository_location + '::test', 'input')
  1618. output = self.cmd('recreate', '--list', '--info', self.repository_location + '::test', '-e', 'input/file2')
  1619. self.check_cache()
  1620. self.assert_in("input/file1", output)
  1621. self.assert_in("x input/file2", output)
  1622. output = self.cmd('recreate', '--list', self.repository_location + '::test', '-e', 'input/file3')
  1623. self.check_cache()
  1624. self.assert_in("input/file1", output)
  1625. self.assert_in("x input/file3", output)
  1626. output = self.cmd('recreate', self.repository_location + '::test', '-e', 'input/file4')
  1627. self.check_cache()
  1628. self.assert_not_in("input/file1", output)
  1629. self.assert_not_in("x input/file4", output)
  1630. output = self.cmd('recreate', '--info', self.repository_location + '::test', '-e', 'input/file5')
  1631. self.check_cache()
  1632. self.assert_not_in("input/file1", output)
  1633. self.assert_not_in("x input/file5", output)
  1634. def test_bad_filters(self):
  1635. self.cmd('init', self.repository_location)
  1636. self.cmd('create', self.repository_location + '::test', 'input')
  1637. self.cmd('delete', '--first', '1', '--last', '1', self.repository_location, fork=True, exit_code=2)
  1638. def test_key_export_keyfile(self):
  1639. export_file = self.output_path + '/exported'
  1640. self.cmd('init', self.repository_location, '--encryption', 'keyfile')
  1641. repo_id = self._extract_repository_id(self.repository_path)
  1642. self.cmd('key', 'export', self.repository_location, export_file)
  1643. with open(export_file, 'r') as fd:
  1644. export_contents = fd.read()
  1645. assert export_contents.startswith('BORG_KEY ' + bin_to_hex(repo_id) + '\n')
  1646. key_file = self.keys_path + '/' + os.listdir(self.keys_path)[0]
  1647. with open(key_file, 'r') as fd:
  1648. key_contents = fd.read()
  1649. assert key_contents == export_contents
  1650. os.unlink(key_file)
  1651. self.cmd('key', 'import', self.repository_location, export_file)
  1652. with open(key_file, 'r') as fd:
  1653. key_contents2 = fd.read()
  1654. assert key_contents2 == key_contents
  1655. def test_key_export_repokey(self):
  1656. export_file = self.output_path + '/exported'
  1657. self.cmd('init', self.repository_location, '--encryption', 'repokey')
  1658. repo_id = self._extract_repository_id(self.repository_path)
  1659. self.cmd('key', 'export', self.repository_location, export_file)
  1660. with open(export_file, 'r') as fd:
  1661. export_contents = fd.read()
  1662. assert export_contents.startswith('BORG_KEY ' + bin_to_hex(repo_id) + '\n')
  1663. with Repository(self.repository_path) as repository:
  1664. repo_key = RepoKey(repository)
  1665. repo_key.load(None, Passphrase.env_passphrase())
  1666. backup_key = KeyfileKey(None)
  1667. backup_key.load(export_file, Passphrase.env_passphrase())
  1668. assert repo_key.enc_key == backup_key.enc_key
  1669. with Repository(self.repository_path) as repository:
  1670. repository.save_key(b'')
  1671. self.cmd('key', 'import', self.repository_location, export_file)
  1672. with Repository(self.repository_path) as repository:
  1673. repo_key2 = RepoKey(repository)
  1674. repo_key2.load(None, Passphrase.env_passphrase())
  1675. assert repo_key2.enc_key == repo_key2.enc_key
  1676. def test_key_import_errors(self):
  1677. export_file = self.output_path + '/exported'
  1678. self.cmd('init', self.repository_location, '--encryption', 'keyfile')
  1679. self.cmd('key', 'import', self.repository_location, export_file, exit_code=EXIT_ERROR)
  1680. with open(export_file, 'w') as fd:
  1681. fd.write('something not a key\n')
  1682. if self.FORK_DEFAULT:
  1683. self.cmd('key', 'import', self.repository_location, export_file, exit_code=2)
  1684. else:
  1685. self.assert_raises(NotABorgKeyFile, lambda: self.cmd('key', 'import', self.repository_location, export_file))
  1686. with open(export_file, 'w') as fd:
  1687. fd.write('BORG_KEY a0a0a0\n')
  1688. if self.FORK_DEFAULT:
  1689. self.cmd('key', 'import', self.repository_location, export_file, exit_code=2)
  1690. else:
  1691. self.assert_raises(RepoIdMismatch, lambda: self.cmd('key', 'import', self.repository_location, export_file))
  1692. def test_key_export_paperkey(self):
  1693. repo_id = 'e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239'
  1694. export_file = self.output_path + '/exported'
  1695. self.cmd('init', self.repository_location, '--encryption', 'keyfile')
  1696. self._set_repository_id(self.repository_path, unhexlify(repo_id))
  1697. key_file = self.keys_path + '/' + os.listdir(self.keys_path)[0]
  1698. with open(key_file, 'w') as fd:
  1699. fd.write(KeyfileKey.FILE_ID + ' ' + repo_id + '\n')
  1700. fd.write(b2a_base64(b'abcdefghijklmnopqrstu').decode())
  1701. self.cmd('key', 'export', '--paper', self.repository_location, export_file)
  1702. with open(export_file, 'r') as fd:
  1703. export_contents = fd.read()
  1704. assert export_contents == """To restore key use borg key import --paper /path/to/repo
  1705. BORG PAPER KEY v1
  1706. id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02
  1707. 1: 616263 646566 676869 6a6b6c 6d6e6f 707172 - 6d
  1708. 2: 737475 - 88
  1709. """
  1710. @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
  1711. class ArchiverTestCaseBinary(ArchiverTestCase):
  1712. EXE = 'borg.exe'
  1713. FORK_DEFAULT = True
  1714. @unittest.skip('patches objects')
  1715. def test_init_interrupt(self):
  1716. pass
  1717. @unittest.skip('test_basic_functionality seems incompatible with fakeroot and/or the binary.')
  1718. def test_basic_functionality(self):
  1719. pass
  1720. @unittest.skip('test_overwrite seems incompatible with fakeroot and/or the binary.')
  1721. def test_overwrite(self):
  1722. pass
  1723. def test_fuse(self):
  1724. if fakeroot_detected():
  1725. unittest.skip('test_fuse with the binary is not compatible with fakeroot')
  1726. else:
  1727. super().test_fuse()
  1728. class ArchiverCheckTestCase(ArchiverTestCaseBase):
  1729. def setUp(self):
  1730. super().setUp()
  1731. with patch.object(ChunkBuffer, 'BUFFER_SIZE', 10):
  1732. self.cmd('init', self.repository_location)
  1733. self.create_src_archive('archive1')
  1734. self.create_src_archive('archive2')
  1735. def test_check_usage(self):
  1736. output = self.cmd('check', '-v', '--progress', self.repository_location, exit_code=0)
  1737. self.assert_in('Starting repository check', output)
  1738. self.assert_in('Starting archive consistency check', output)
  1739. self.assert_in('Checking segments', output)
  1740. # reset logging to new process default to avoid need for fork=True on next check
  1741. logging.getLogger('borg.output.progress').setLevel(logging.NOTSET)
  1742. output = self.cmd('check', '-v', '--repository-only', self.repository_location, exit_code=0)
  1743. self.assert_in('Starting repository check', output)
  1744. self.assert_not_in('Starting archive consistency check', output)
  1745. self.assert_not_in('Checking segments', output)
  1746. output = self.cmd('check', '-v', '--archives-only', self.repository_location, exit_code=0)
  1747. self.assert_not_in('Starting repository check', output)
  1748. self.assert_in('Starting archive consistency check', output)
  1749. output = self.cmd('check', '-v', '--archives-only', '--prefix=archive2', self.repository_location, exit_code=0)
  1750. self.assert_not_in('archive1', output)
  1751. output = self.cmd('check', '-v', '--archives-only', '--first=1', self.repository_location, exit_code=0)
  1752. self.assert_in('archive1', output)
  1753. self.assert_not_in('archive2', output)
  1754. output = self.cmd('check', '-v', '--archives-only', '--last=1', self.repository_location, exit_code=0)
  1755. self.assert_not_in('archive1', output)
  1756. self.assert_in('archive2', output)
  1757. def test_missing_file_chunk(self):
  1758. archive, repository = self.open_archive('archive1')
  1759. with repository:
  1760. for item in archive.iter_items():
  1761. if item.path.endswith('testsuite/archiver.py'):
  1762. valid_chunks = item.chunks
  1763. killed_chunk = valid_chunks[-1]
  1764. repository.delete(killed_chunk.id)
  1765. break
  1766. else:
  1767. self.assert_true(False) # should not happen
  1768. repository.commit()
  1769. self.cmd('check', self.repository_location, exit_code=1)
  1770. output = self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1771. self.assert_in('New missing file chunk detected', output)
  1772. self.cmd('check', self.repository_location, exit_code=0)
  1773. # check that the file in the old archives has now a different chunk list without the killed chunk
  1774. for archive_name in ('archive1', 'archive2'):
  1775. archive, repository = self.open_archive(archive_name)
  1776. with repository:
  1777. for item in archive.iter_items():
  1778. if item.path.endswith('testsuite/archiver.py'):
  1779. self.assert_not_equal(valid_chunks, item.chunks)
  1780. self.assert_not_in(killed_chunk, item.chunks)
  1781. break
  1782. else:
  1783. self.assert_true(False) # should not happen
  1784. # do a fresh backup (that will include the killed chunk)
  1785. with patch.object(ChunkBuffer, 'BUFFER_SIZE', 10):
  1786. self.create_src_archive('archive3')
  1787. # check should be able to heal the file now:
  1788. output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0)
  1789. self.assert_in('Healed previously missing file chunk', output)
  1790. self.assert_in('testsuite/archiver.py: Completely healed previously damaged file!', output)
  1791. # check that the file in the old archives has the correct chunks again
  1792. for archive_name in ('archive1', 'archive2'):
  1793. archive, repository = self.open_archive(archive_name)
  1794. with repository:
  1795. for item in archive.iter_items():
  1796. if item.path.endswith('testsuite/archiver.py'):
  1797. self.assert_equal(valid_chunks, item.chunks)
  1798. break
  1799. else:
  1800. self.assert_true(False) # should not happen
  1801. def test_missing_archive_item_chunk(self):
  1802. archive, repository = self.open_archive('archive1')
  1803. with repository:
  1804. repository.delete(archive.metadata.items[-5])
  1805. repository.commit()
  1806. self.cmd('check', self.repository_location, exit_code=1)
  1807. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1808. self.cmd('check', self.repository_location, exit_code=0)
  1809. def test_missing_archive_metadata(self):
  1810. archive, repository = self.open_archive('archive1')
  1811. with repository:
  1812. repository.delete(archive.id)
  1813. repository.commit()
  1814. self.cmd('check', self.repository_location, exit_code=1)
  1815. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1816. self.cmd('check', self.repository_location, exit_code=0)
  1817. def test_missing_manifest(self):
  1818. archive, repository = self.open_archive('archive1')
  1819. with repository:
  1820. repository.delete(Manifest.MANIFEST_ID)
  1821. repository.commit()
  1822. self.cmd('check', self.repository_location, exit_code=1)
  1823. output = self.cmd('check', '-v', '--repair', self.repository_location, exit_code=0)
  1824. self.assert_in('archive1', output)
  1825. self.assert_in('archive2', output)
  1826. self.cmd('check', self.repository_location, exit_code=0)
  1827. def test_extra_chunks(self):
  1828. self.cmd('check', self.repository_location, exit_code=0)
  1829. with Repository(self.repository_location, exclusive=True) as repository:
  1830. repository.put(b'01234567890123456789012345678901', b'xxxx')
  1831. repository.commit()
  1832. self.cmd('check', self.repository_location, exit_code=1)
  1833. self.cmd('check', self.repository_location, exit_code=1)
  1834. self.cmd('check', '--repair', self.repository_location, exit_code=0)
  1835. self.cmd('check', self.repository_location, exit_code=0)
  1836. self.cmd('extract', '--dry-run', self.repository_location + '::archive1', exit_code=0)
  1837. def _test_verify_data(self, *init_args):
  1838. shutil.rmtree(self.repository_path)
  1839. self.cmd('init', self.repository_location, *init_args)
  1840. self.create_src_archive('archive1')
  1841. archive, repository = self.open_archive('archive1')
  1842. with repository:
  1843. for item in archive.iter_items():
  1844. if item.path.endswith('testsuite/archiver.py'):
  1845. chunk = item.chunks[-1]
  1846. data = repository.get(chunk.id) + b'1234'
  1847. repository.put(chunk.id, data)
  1848. break
  1849. repository.commit()
  1850. self.cmd('check', self.repository_location, exit_code=0)
  1851. output = self.cmd('check', '--verify-data', self.repository_location, exit_code=1)
  1852. assert bin_to_hex(chunk.id) + ', integrity error' in output
  1853. # repair (heal is tested in another test)
  1854. output = self.cmd('check', '--repair', '--verify-data', self.repository_location, exit_code=0)
  1855. assert bin_to_hex(chunk.id) + ', integrity error' in output
  1856. assert 'testsuite/archiver.py: New missing file chunk detected' in output
  1857. def test_verify_data(self):
  1858. self._test_verify_data('--encryption', 'repokey')
  1859. def test_verify_data_unencrypted(self):
  1860. self._test_verify_data('--encryption', 'none')
  1861. def test_empty_repository(self):
  1862. with Repository(self.repository_location, exclusive=True) as repository:
  1863. for id_ in repository.list():
  1864. repository.delete(id_)
  1865. repository.commit()
  1866. self.cmd('check', self.repository_location, exit_code=1)
  1867. def test_attic013_acl_bug(self):
  1868. # Attic up to release 0.13 contained a bug where every item unintentionally received
  1869. # a b'acl'=None key-value pair.
  1870. # This bug can still live on in Borg repositories (through borg upgrade).
  1871. class Attic013Item:
  1872. def as_dict():
  1873. return {
  1874. # These are required
  1875. b'path': '1234',
  1876. b'mtime': 0,
  1877. b'mode': 0,
  1878. b'user': b'0',
  1879. b'group': b'0',
  1880. b'uid': 0,
  1881. b'gid': 0,
  1882. # acl is the offending key.
  1883. b'acl': None,
  1884. }
  1885. archive, repository = self.open_archive('archive1')
  1886. with repository:
  1887. manifest, key = Manifest.load(repository)
  1888. with Cache(repository, key, manifest) as cache:
  1889. archive = Archive(repository, key, manifest, '0.13', cache=cache, create=True)
  1890. archive.items_buffer.add(Attic013Item)
  1891. archive.save()
  1892. self.cmd('check', self.repository_location, exit_code=0)
  1893. self.cmd('list', self.repository_location + '::0.13', exit_code=0)
  1894. @pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs')
  1895. class RemoteArchiverTestCase(ArchiverTestCase):
  1896. prefix = '__testsuite__:'
  1897. def open_repository(self):
  1898. return RemoteRepository(Location(self.repository_location))
  1899. def test_remote_repo_restrict_to_path(self):
  1900. # restricted to repo directory itself:
  1901. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', self.repository_path]):
  1902. self.cmd('init', self.repository_location)
  1903. # restricted to repo directory itself, fail for other directories with same prefix:
  1904. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', self.repository_path]):
  1905. self.assert_raises(PathNotAllowed, lambda: self.cmd('init', self.repository_location + '_0'))
  1906. # restricted to a completely different path:
  1907. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo']):
  1908. self.assert_raises(PathNotAllowed, lambda: self.cmd('init', self.repository_location + '_1'))
  1909. path_prefix = os.path.dirname(self.repository_path)
  1910. # restrict to repo directory's parent directory:
  1911. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', path_prefix]):
  1912. self.cmd('init', self.repository_location + '_2')
  1913. # restrict to repo directory's parent directory and another directory:
  1914. with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]):
  1915. self.cmd('init', self.repository_location + '_3')
  1916. @unittest.skip('only works locally')
  1917. def test_debug_put_get_delete_obj(self):
  1918. pass
  1919. def test_strip_components_doesnt_leak(self):
  1920. self.cmd('init', self.repository_location)
  1921. self.create_regular_file('dir/file', contents=b"test file contents 1")
  1922. self.create_regular_file('dir/file2', contents=b"test file contents 2")
  1923. self.create_regular_file('skipped-file1', contents=b"test file contents 3")
  1924. self.create_regular_file('skipped-file2', contents=b"test file contents 4")
  1925. self.create_regular_file('skipped-file3', contents=b"test file contents 5")
  1926. self.cmd('create', self.repository_location + '::test', 'input')
  1927. marker = 'cached responses left in RemoteRepository'
  1928. with changedir('output'):
  1929. res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '3')
  1930. self.assert_true(marker not in res)
  1931. with self.assert_creates_file('file'):
  1932. res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '2')
  1933. self.assert_true(marker not in res)
  1934. with self.assert_creates_file('dir/file'):
  1935. res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '1')
  1936. self.assert_true(marker not in res)
  1937. with self.assert_creates_file('input/dir/file'):
  1938. res = self.cmd('extract', "--debug", self.repository_location + '::test', '--strip-components', '0')
  1939. self.assert_true(marker not in res)
  1940. class DiffArchiverTestCase(ArchiverTestCaseBase):
  1941. def test_basic_functionality(self):
  1942. # Initialize test folder
  1943. self.create_test_files()
  1944. self.cmd('init', self.repository_location)
  1945. # Setup files for the first snapshot
  1946. self.create_regular_file('file_unchanged', size=128)
  1947. self.create_regular_file('file_removed', size=256)
  1948. self.create_regular_file('file_removed2', size=512)
  1949. self.create_regular_file('file_replaced', size=1024)
  1950. os.mkdir('input/dir_replaced_with_file')
  1951. os.chmod('input/dir_replaced_with_file', stat.S_IFDIR | 0o755)
  1952. os.mkdir('input/dir_removed')
  1953. if are_symlinks_supported():
  1954. os.mkdir('input/dir_replaced_with_link')
  1955. os.symlink('input/dir_replaced_with_file', 'input/link_changed')
  1956. os.symlink('input/file_unchanged', 'input/link_removed')
  1957. os.symlink('input/file_removed2', 'input/link_target_removed')
  1958. os.symlink('input/empty', 'input/link_target_contents_changed')
  1959. os.symlink('input/empty', 'input/link_replaced_by_file')
  1960. if are_hardlinks_supported():
  1961. os.link('input/empty', 'input/hardlink_contents_changed')
  1962. os.link('input/file_removed', 'input/hardlink_removed')
  1963. os.link('input/file_removed2', 'input/hardlink_target_removed')
  1964. os.link('input/file_replaced', 'input/hardlink_target_replaced')
  1965. # Create the first snapshot
  1966. self.cmd('create', self.repository_location + '::test0', 'input')
  1967. # Setup files for the second snapshot
  1968. self.create_regular_file('file_added', size=2048)
  1969. os.unlink('input/file_removed')
  1970. os.unlink('input/file_removed2')
  1971. os.unlink('input/file_replaced')
  1972. self.create_regular_file('file_replaced', size=4096, contents=b'0')
  1973. os.rmdir('input/dir_replaced_with_file')
  1974. self.create_regular_file('dir_replaced_with_file', size=8192)
  1975. os.chmod('input/dir_replaced_with_file', stat.S_IFREG | 0o755)
  1976. os.mkdir('input/dir_added')
  1977. os.rmdir('input/dir_removed')
  1978. if are_symlinks_supported():
  1979. os.rmdir('input/dir_replaced_with_link')
  1980. os.symlink('input/dir_added', 'input/dir_replaced_with_link')
  1981. os.unlink('input/link_changed')
  1982. os.symlink('input/dir_added', 'input/link_changed')
  1983. os.symlink('input/dir_added', 'input/link_added')
  1984. os.unlink('input/link_replaced_by_file')
  1985. self.create_regular_file('link_replaced_by_file', size=16384)
  1986. os.unlink('input/link_removed')
  1987. if are_hardlinks_supported():
  1988. os.unlink('input/hardlink_removed')
  1989. os.link('input/file_added', 'input/hardlink_added')
  1990. with open('input/empty', 'ab') as fd:
  1991. fd.write(b'appended_data')
  1992. # Create the second snapshot
  1993. self.cmd('create', self.repository_location + '::test1a', 'input')
  1994. self.cmd('create', '--chunker-params', '16,18,17,4095', self.repository_location + '::test1b', 'input')
  1995. def do_asserts(output, archive):
  1996. # File contents changed (deleted and replaced with a new file)
  1997. assert 'B input/file_replaced' in output
  1998. # File unchanged
  1999. assert 'input/file_unchanged' not in output
  2000. # Directory replaced with a regular file
  2001. if 'BORG_TESTS_IGNORE_MODES' not in os.environ:
  2002. assert '[drwxr-xr-x -> -rwxr-xr-x] input/dir_replaced_with_file' in output
  2003. # Basic directory cases
  2004. assert 'added directory input/dir_added' in output
  2005. assert 'removed directory input/dir_removed' in output
  2006. if are_symlinks_supported():
  2007. # Basic symlink cases
  2008. assert 'changed link input/link_changed' in output
  2009. assert 'added link input/link_added' in output
  2010. assert 'removed link input/link_removed' in output
  2011. # Symlink replacing or being replaced
  2012. assert '] input/dir_replaced_with_link' in output
  2013. assert '] input/link_replaced_by_file' in output
  2014. # Symlink target removed. Should not affect the symlink at all.
  2015. assert 'input/link_target_removed' not in output
  2016. # The inode has two links and the file contents changed. Borg
  2017. # should notice the changes in both links. However, the symlink
  2018. # pointing to the file is not changed.
  2019. assert '0 B input/empty' in output
  2020. if are_hardlinks_supported():
  2021. assert '0 B input/hardlink_contents_changed' in output
  2022. if are_symlinks_supported():
  2023. assert 'input/link_target_contents_changed' not in output
  2024. # Added a new file and a hard link to it. Both links to the same
  2025. # inode should appear as separate files.
  2026. assert 'added 2.05 kB input/file_added' in output
  2027. if are_hardlinks_supported():
  2028. assert 'added 2.05 kB input/hardlink_added' in output
  2029. # The inode has two links and both of them are deleted. They should
  2030. # appear as two deleted files.
  2031. assert 'removed 256 B input/file_removed' in output
  2032. if are_hardlinks_supported():
  2033. assert 'removed 256 B input/hardlink_removed' in output
  2034. # Another link (marked previously as the source in borg) to the
  2035. # same inode was removed. This should not change this link at all.
  2036. if are_hardlinks_supported():
  2037. assert 'input/hardlink_target_removed' not in output
  2038. # Another link (marked previously as the source in borg) to the
  2039. # same inode was replaced with a new regular file. This should not
  2040. # change this link at all.
  2041. if are_hardlinks_supported():
  2042. assert 'input/hardlink_target_replaced' not in output
  2043. do_asserts(self.cmd('diff', self.repository_location + '::test0', 'test1a'), '1a')
  2044. # We expect exit_code=1 due to the chunker params warning
  2045. do_asserts(self.cmd('diff', self.repository_location + '::test0', 'test1b', exit_code=1), '1b')
  2046. def test_sort_option(self):
  2047. self.cmd('init', self.repository_location)
  2048. self.create_regular_file('a_file_removed', size=8)
  2049. self.create_regular_file('f_file_removed', size=16)
  2050. self.create_regular_file('c_file_changed', size=32)
  2051. self.create_regular_file('e_file_changed', size=64)
  2052. self.cmd('create', self.repository_location + '::test0', 'input')
  2053. os.unlink('input/a_file_removed')
  2054. os.unlink('input/f_file_removed')
  2055. os.unlink('input/c_file_changed')
  2056. os.unlink('input/e_file_changed')
  2057. self.create_regular_file('c_file_changed', size=512)
  2058. self.create_regular_file('e_file_changed', size=1024)
  2059. self.create_regular_file('b_file_added', size=128)
  2060. self.create_regular_file('d_file_added', size=256)
  2061. self.cmd('create', self.repository_location + '::test1', 'input')
  2062. output = self.cmd('diff', '--sort', self.repository_location + '::test0', 'test1')
  2063. expected = [
  2064. 'a_file_removed',
  2065. 'b_file_added',
  2066. 'c_file_changed',
  2067. 'd_file_added',
  2068. 'e_file_changed',
  2069. 'f_file_removed',
  2070. ]
  2071. assert all(x in line for x, line in zip(expected, output.splitlines()))
  2072. def test_get_args():
  2073. archiver = Archiver()
  2074. # everything normal:
  2075. # first param is argv as produced by ssh forced command,
  2076. # second param is like from SSH_ORIGINAL_COMMAND env variable
  2077. args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
  2078. 'borg serve --info --umask=0027')
  2079. assert args.func == archiver.do_serve
  2080. assert args.restrict_to_paths == ['/p1', '/p2']
  2081. assert args.umask == 0o027
  2082. assert args.log_level == 'info'
  2083. # trying to cheat - break out of path restriction
  2084. args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
  2085. 'borg serve --restrict-to-path=/')
  2086. assert args.restrict_to_paths == ['/p1', '/p2']
  2087. # trying to cheat - try to execute different subcommand
  2088. args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
  2089. 'borg init /')
  2090. assert args.func == archiver.do_serve
  2091. def test_compare_chunk_contents():
  2092. def ccc(a, b):
  2093. chunks_a = [Chunk(data) for data in a]
  2094. chunks_b = [Chunk(data) for data in b]
  2095. compare1 = Archiver.compare_chunk_contents(iter(chunks_a), iter(chunks_b))
  2096. compare2 = Archiver.compare_chunk_contents(iter(chunks_b), iter(chunks_a))
  2097. assert compare1 == compare2
  2098. return compare1
  2099. assert ccc([
  2100. b'1234', b'567A', b'bC'
  2101. ], [
  2102. b'1', b'23', b'4567A', b'b', b'C'
  2103. ])
  2104. # one iterator exhausted before the other
  2105. assert not ccc([
  2106. b'12345',
  2107. ], [
  2108. b'1234', b'56'
  2109. ])
  2110. # content mismatch
  2111. assert not ccc([
  2112. b'1234', b'65'
  2113. ], [
  2114. b'1234', b'56'
  2115. ])
  2116. # first is the prefix of second
  2117. assert not ccc([
  2118. b'1234', b'56'
  2119. ], [
  2120. b'1234', b'565'
  2121. ])
  2122. class TestBuildFilter:
  2123. @staticmethod
  2124. def peek_and_store_hardlink_masters(item, matched):
  2125. pass
  2126. def test_basic(self):
  2127. matcher = PatternMatcher()
  2128. matcher.add([parse_pattern('included')], True)
  2129. filter = Archiver.build_filter(matcher, self.peek_and_store_hardlink_masters, 0)
  2130. assert filter(Item(path='included'))
  2131. assert filter(Item(path='included/file'))
  2132. assert not filter(Item(path='something else'))
  2133. def test_empty(self):
  2134. matcher = PatternMatcher(fallback=True)
  2135. filter = Archiver.build_filter(matcher, self.peek_and_store_hardlink_masters, 0)
  2136. assert filter(Item(path='anything'))
  2137. def test_strip_components(self):
  2138. matcher = PatternMatcher(fallback=True)
  2139. filter = Archiver.build_filter(matcher, self.peek_and_store_hardlink_masters, strip_components=1)
  2140. assert not filter(Item(path='shallow'))
  2141. assert not filter(Item(path='shallow/')) # can this even happen? paths are normalized...
  2142. assert filter(Item(path='deep enough/file'))
  2143. assert filter(Item(path='something/dir/file'))